ssb-sgis 0.2.6__py3-none-any.whl → 0.2.8__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.
sgis/__init__.py CHANGED
@@ -3,7 +3,9 @@ from .geopandas_tools.buffer_dissolve_explode import (
3
3
  buff,
4
4
  buffdiss,
5
5
  buffdissexp,
6
+ buffdissexp_by_cluster,
6
7
  dissexp,
8
+ dissexp_by_cluster,
7
9
  )
8
10
  from .geopandas_tools.general import (
9
11
  bounds_to_points,
@@ -23,6 +25,7 @@ from .geopandas_tools.general import (
23
25
  from .geopandas_tools.geometry_types import (
24
26
  get_geom_type,
25
27
  is_single_geom_type,
28
+ make_all_singlepart,
26
29
  to_single_geom_type,
27
30
  )
28
31
  from .geopandas_tools.neighbors import (
@@ -46,6 +49,7 @@ from .geopandas_tools.polygon_operations import (
46
49
  )
47
50
  from .geopandas_tools.to_geodataframe import to_gdf
48
51
  from .helpers import get_name
52
+ from .maps.examine import Examine
49
53
  from .maps.explore import Explore
50
54
  from .maps.httpserver import run_html_server
51
55
  from .maps.legend import Legend
@@ -17,6 +17,7 @@ for the following:
17
17
  from geopandas import GeoDataFrame, GeoSeries
18
18
 
19
19
  from .geometry_types import make_all_singlepart
20
+ from .polygon_operations import get_polygon_clusters
20
21
 
21
22
 
22
23
  def _decide_ignore_index(kwargs: dict) -> tuple[dict, bool]:
@@ -190,6 +191,31 @@ def dissexp(
190
191
  )
191
192
 
192
193
 
194
+ def dissexp_by_cluster(gdf: GeoDataFrame) -> GeoDataFrame:
195
+ return (
196
+ gdf.explode(ignore_index=True)
197
+ .pipe(get_polygon_clusters, cluster_col="cluster")
198
+ .pipe(dissexp, by="cluster")
199
+ .reset_index(drop=True)
200
+ )
201
+
202
+
203
+ def buffdissexp_by_cluster(
204
+ gdf: GeoDataFrame,
205
+ distance: int | float,
206
+ *,
207
+ resolution: int = 50,
208
+ copy: bool = True,
209
+ ) -> GeoDataFrame:
210
+ return (
211
+ buff(gdf, distance, resolution=resolution, copy=copy)
212
+ .explode(ignore_index=True)
213
+ .pipe(get_polygon_clusters, cluster_col="cluster")
214
+ .pipe(dissexp, by="cluster")
215
+ .reset_index(drop=True)
216
+ )
217
+
218
+
193
219
  def buff(
194
220
  gdf: GeoDataFrame | GeoSeries,
195
221
  distance: int | float,
@@ -2,6 +2,7 @@ import warnings
2
2
 
3
3
  import geopandas as gpd
4
4
  import numpy as np
5
+ import pandas as pd
5
6
  from geopandas import GeoDataFrame, GeoSeries
6
7
  from geopandas.array import GeometryDtype
7
8
  from shapely import (
@@ -519,11 +520,17 @@ def to_lines(*gdfs: GeoDataFrame, copy: bool = True) -> GeoDataFrame:
519
520
  if len(lines) == 1:
520
521
  return lines[0]
521
522
 
522
- unioned = lines[0].overlay(lines[1], how="union", keep_geom_type=True)
523
+ if len(lines[0]) and len(lines[1]):
524
+ unioned = lines[0].overlay(lines[1], how="union", keep_geom_type=True)
525
+ else:
526
+ unioned = pd.concat([lines[0], lines[1]], ignore_index=True)
523
527
 
524
528
  if len(lines) > 2:
525
529
  for line_gdf in lines[2:]:
526
- unioned = unioned.overlay(line_gdf, how="union", keep_geom_type=True)
530
+ if len(line_gdf):
531
+ unioned = unioned.overlay(line_gdf, how="union", keep_geom_type=True)
532
+ else:
533
+ unioned = pd.concat([unioned, line_gdf], ignore_index=True)
527
534
 
528
535
  return make_all_singlepart(unioned, ignore_index=True)
529
536
 
@@ -1,4 +1,5 @@
1
1
  """Functions for polygon geometries."""
2
+ import functools
2
3
  import warnings
3
4
 
4
5
  import geopandas as gpd
@@ -8,6 +9,7 @@ import pandas as pd
8
9
  from geopandas import GeoDataFrame, GeoSeries
9
10
  from shapely import (
10
11
  area,
12
+ difference,
11
13
  get_exterior_ring,
12
14
  get_interior_ring,
13
15
  get_num_interior_rings,
@@ -21,6 +23,147 @@ from .neighbors import get_neighbor_indices
21
23
  from .overlay import clean_overlay
22
24
 
23
25
 
26
+ def get_polygon_clusters(
27
+ *gdfs: GeoDataFrame | GeoSeries,
28
+ cluster_col: str = "cluster",
29
+ allow_multipart: bool = False,
30
+ ) -> GeoDataFrame | tuple[GeoDataFrame]:
31
+ """Find which polygons overlap without dissolving.
32
+
33
+ Devides polygons into clusters in a fast and precice manner by using spatial join
34
+ and networkx to find the connected components, i.e. overlapping geometries.
35
+ If multiple GeoDataFrames are given, the clusters will be based on all
36
+ combined.
37
+
38
+ This can be used instead of dissolve+explode, or before dissolving by the cluster
39
+ column. This has been tested to be a lot faster if there are many
40
+ non-overlapping polygons, but somewhat slower than dissolve+explode if most
41
+ polygons overlap.
42
+
43
+ Args:
44
+ gdfs: One or more GeoDataFrames of polygons.
45
+ cluster_col: Name of the resulting cluster column.
46
+ allow_multipart: Whether to allow mutipart geometries in the gdfs.
47
+ Defaults to False to avoid confusing results.
48
+
49
+ Returns:
50
+ One or more GeoDataFrames (same amount as was given) with a new cluster column.
51
+
52
+ Examples
53
+ --------
54
+
55
+ Create geometries with three clusters of overlapping polygons.
56
+
57
+ >>> import sgis as sg
58
+ >>> gdf = sg.to_gdf([(0, 0), (1, 1), (0, 1), (4, 4), (4, 3), (7, 7)])
59
+ >>> buffered = sg.buff(gdf, 1)
60
+ >>> gdf
61
+ geometry
62
+ 0 POLYGON ((1.00000 0.00000, 0.99951 -0.03141, 0...
63
+ 1 POLYGON ((2.00000 1.00000, 1.99951 0.96859, 1....
64
+ 2 POLYGON ((1.00000 1.00000, 0.99951 0.96859, 0....
65
+ 3 POLYGON ((5.00000 4.00000, 4.99951 3.96859, 4....
66
+ 4 POLYGON ((5.00000 3.00000, 4.99951 2.96859, 4....
67
+ 5 POLYGON ((8.00000 7.00000, 7.99951 6.96859, 7....
68
+
69
+ Add a cluster column to the GeoDataFrame:
70
+
71
+ >>> gdf = sg.get_polygon_clusters(gdf, cluster_col="cluster")
72
+ >>> gdf
73
+ cluster geometry
74
+ 0 0 POLYGON ((1.00000 0.00000, 0.99951 -0.03141, 0...
75
+ 1 0 POLYGON ((2.00000 1.00000, 1.99951 0.96859, 1....
76
+ 2 0 POLYGON ((1.00000 1.00000, 0.99951 0.96859, 0....
77
+ 3 1 POLYGON ((5.00000 4.00000, 4.99951 3.96859, 4....
78
+ 4 1 POLYGON ((5.00000 3.00000, 4.99951 2.96859, 4....
79
+ 5 2 POLYGON ((8.00000 7.00000, 7.99951 6.96859, 7....
80
+
81
+ If multiple GeoDataFrames are given, all are returned with common
82
+ cluster values.
83
+
84
+ >>> gdf2 = sg.to_gdf([(0, 0), (7, 7)])
85
+ >>> gdf, gdf2 = sg.get_polygon_clusters(gdf, gdf2, cluster_col="cluster")
86
+ >>> gdf2
87
+ cluster geometry
88
+ 0 0 POINT (0.00000 0.00000)
89
+ 1 2 POINT (7.00000 7.00000)
90
+ >>> gdf
91
+ cluster geometry
92
+ 0 0 POLYGON ((1.00000 0.00000, 0.99951 -0.03141, 0...
93
+ 1 0 POLYGON ((2.00000 1.00000, 1.99951 0.96859, 1....
94
+ 2 0 POLYGON ((1.00000 1.00000, 0.99951 0.96859, 0....
95
+ 3 1 POLYGON ((5.00000 4.00000, 4.99951 3.96859, 4....
96
+ 4 1 POLYGON ((5.00000 3.00000, 4.99951 2.96859, 4....
97
+ 5 2 POLYGON ((8.00000 7.00000, 7.99951 6.96859, 7....
98
+
99
+ Dissolving 'by' the cluster column will make the dissolve much
100
+ faster if there are a lot of non-overlapping polygons.
101
+
102
+ >>> dissolved = gdf.dissolve(by="cluster", as_index=False)
103
+ >>> dissolved
104
+ cluster geometry
105
+ 0 0 POLYGON ((0.99951 -0.03141, 0.99803 -0.06279, ...
106
+ 1 1 POLYGON ((4.99951 2.96859, 4.99803 2.93721, 4....
107
+ 2 2 POLYGON ((8.00000 7.00000, 7.99951 6.96859, 7....
108
+ """
109
+ if isinstance(gdfs[-1], str):
110
+ *gdfs, cluster_col = gdfs
111
+
112
+ concated = pd.DataFrame()
113
+ orig_indices = ()
114
+ for i, gdf in enumerate(gdfs):
115
+ if isinstance(gdf, GeoSeries):
116
+ gdf = gdf.to_frame()
117
+
118
+ if not isinstance(gdf, GeoDataFrame):
119
+ raise TypeError("'gdfs' should be GeoDataFrames or GeoSeries.")
120
+
121
+ if not allow_multipart and len(gdf) != len(gdf.explode(index_parts=False)):
122
+ raise ValueError(
123
+ "All geometries should be exploded to singlepart "
124
+ "in order to get correct polygon clusters. "
125
+ "To allow multipart geometries, set allow_multipart=True"
126
+ )
127
+
128
+ orig_indices = orig_indices + (gdf.index,)
129
+
130
+ gdf["i__"] = i
131
+
132
+ concated = pd.concat([concated, gdf], ignore_index=True)
133
+
134
+ neighbors = get_neighbor_indices(concated, concated)
135
+
136
+ edges = [(source, target) for source, target in neighbors.items()]
137
+
138
+ graph = nx.Graph()
139
+ graph.add_edges_from(edges)
140
+
141
+ component_mapper = {
142
+ j: i
143
+ for i, component in enumerate(nx.connected_components(graph))
144
+ for j in component
145
+ }
146
+
147
+ concated[cluster_col] = component_mapper
148
+
149
+ concated = _push_geom_col(concated)
150
+
151
+ n_gdfs = concated["i__"].unique()
152
+
153
+ if len(n_gdfs) == 1:
154
+ concated.index = orig_indices[0]
155
+ return concated.drop(["i__"], axis=1)
156
+
157
+ unconcated = ()
158
+ for i in n_gdfs:
159
+ gdf = concated[concated["i__"] == i]
160
+ gdf.index = orig_indices[i]
161
+ gdf = gdf.drop(["i__"], axis=1)
162
+ unconcated = unconcated + (gdf,)
163
+
164
+ return unconcated
165
+
166
+
24
167
  def eliminate_by_longest(
25
168
  gdf: GeoDataFrame,
26
169
  to_eliminate: GeoDataFrame,
@@ -213,146 +356,6 @@ def _eliminate_by_area(
213
356
  return eliminated
214
357
 
215
358
 
216
- def get_polygon_clusters(
217
- *gdfs: GeoDataFrame | GeoSeries,
218
- cluster_col: str = "cluster",
219
- allow_multipart: bool = False,
220
- ) -> GeoDataFrame | tuple[GeoDataFrame]:
221
- """Find which polygons overlap without dissolving.
222
-
223
- Devides polygons into clusters in a fast and precice manner by using spatial join
224
- and networkx to find the connected components, i.e. overlapping geometries.
225
- If multiple GeoDataFrames are given, the clusters will be based on all
226
- combined.
227
-
228
- This can be used instead of dissolve+explode, or before dissolving by the cluster
229
- column. This has been tested to be a lot faster if there are many
230
- non-overlapping polygons, but somewhat slower than dissolve+explode if most
231
- polygons overlap.
232
-
233
- Args:
234
- gdfs: One or more GeoDataFrames of polygons.
235
- cluster_col: Name of the resulting cluster column.
236
- allow_multipart: Whether to allow mutipart geometries in the gdfs.
237
- Defaults to False to avoid confusing results.
238
-
239
- Returns:
240
- One or more GeoDataFrames (same amount as was given) with a new cluster column.
241
-
242
- Examples
243
- --------
244
-
245
- Create geometries with three clusters of overlapping polygons.
246
-
247
- >>> import sgis as sg
248
- >>> gdf = sg.to_gdf([(0, 0), (1, 1), (0, 1), (4, 4), (4, 3), (7, 7)])
249
- >>> buffered = sg.buff(gdf, 1)
250
- >>> gdf
251
- geometry
252
- 0 POLYGON ((1.00000 0.00000, 0.99951 -0.03141, 0...
253
- 1 POLYGON ((2.00000 1.00000, 1.99951 0.96859, 1....
254
- 2 POLYGON ((1.00000 1.00000, 0.99951 0.96859, 0....
255
- 3 POLYGON ((5.00000 4.00000, 4.99951 3.96859, 4....
256
- 4 POLYGON ((5.00000 3.00000, 4.99951 2.96859, 4....
257
- 5 POLYGON ((8.00000 7.00000, 7.99951 6.96859, 7....
258
-
259
- Add a cluster column to the GeoDataFrame:
260
-
261
- >>> gdf = sg.get_polygon_clusters(gdf, cluster_col="cluster")
262
- >>> gdf
263
- cluster geometry
264
- 0 0 POLYGON ((1.00000 0.00000, 0.99951 -0.03141, 0...
265
- 1 0 POLYGON ((2.00000 1.00000, 1.99951 0.96859, 1....
266
- 2 0 POLYGON ((1.00000 1.00000, 0.99951 0.96859, 0....
267
- 3 1 POLYGON ((5.00000 4.00000, 4.99951 3.96859, 4....
268
- 4 1 POLYGON ((5.00000 3.00000, 4.99951 2.96859, 4....
269
- 5 2 POLYGON ((8.00000 7.00000, 7.99951 6.96859, 7....
270
-
271
- If multiple GeoDataFrames are given, all are returned with common
272
- cluster values.
273
-
274
- >>> gdf2 = sg.to_gdf([(0, 0), (7, 7)])
275
- >>> gdf, gdf2 = sg.get_polygon_clusters(gdf, gdf2, cluster_col="cluster")
276
- >>> gdf2
277
- cluster geometry
278
- 0 0 POINT (0.00000 0.00000)
279
- 1 2 POINT (7.00000 7.00000)
280
- >>> gdf
281
- cluster geometry
282
- 0 0 POLYGON ((1.00000 0.00000, 0.99951 -0.03141, 0...
283
- 1 0 POLYGON ((2.00000 1.00000, 1.99951 0.96859, 1....
284
- 2 0 POLYGON ((1.00000 1.00000, 0.99951 0.96859, 0....
285
- 3 1 POLYGON ((5.00000 4.00000, 4.99951 3.96859, 4....
286
- 4 1 POLYGON ((5.00000 3.00000, 4.99951 2.96859, 4....
287
- 5 2 POLYGON ((8.00000 7.00000, 7.99951 6.96859, 7....
288
-
289
- Dissolving 'by' the cluster column will make the dissolve much
290
- faster if there are a lot of non-overlapping polygons.
291
-
292
- >>> dissolved = gdf.dissolve(by="cluster", as_index=False)
293
- >>> dissolved
294
- cluster geometry
295
- 0 0 POLYGON ((0.99951 -0.03141, 0.99803 -0.06279, ...
296
- 1 1 POLYGON ((4.99951 2.96859, 4.99803 2.93721, 4....
297
- 2 2 POLYGON ((8.00000 7.00000, 7.99951 6.96859, 7....
298
- """
299
- if isinstance(gdfs[-1], str):
300
- *gdfs, cluster_col = gdfs
301
-
302
- concated = pd.DataFrame()
303
- orig_indices = ()
304
- for i, gdf in enumerate(gdfs):
305
- if isinstance(gdf, GeoSeries):
306
- gdf = gdf.to_frame()
307
-
308
- if not isinstance(gdf, GeoDataFrame):
309
- raise TypeError("'gdfs' should be one or more GeoDataFrames or GeoSeries.")
310
-
311
- if not allow_multipart and len(gdf) != len(gdf.explode(index_parts=False)):
312
- raise ValueError(
313
- "All geometries should be exploded to singlepart "
314
- "in order to get correct polygon clusters. "
315
- "To allow multipart geometries, set allow_multipart=True"
316
- )
317
-
318
- orig_indices = orig_indices + (gdf.index,)
319
- gdf["i__"] = i
320
-
321
- concated = pd.concat([concated, gdf], ignore_index=True)
322
-
323
- neighbors = get_neighbor_indices(concated, concated)
324
-
325
- edges = [(source, target) for source, target in neighbors.items()]
326
-
327
- graph = nx.Graph()
328
- graph.add_edges_from(edges)
329
-
330
- component_mapper = {
331
- j: i
332
- for i, component in enumerate(nx.connected_components(graph))
333
- for j in component
334
- }
335
-
336
- concated[cluster_col] = component_mapper
337
-
338
- concated = _push_geom_col(concated)
339
-
340
- n_gdfs = concated["i__"].unique()
341
-
342
- if len(n_gdfs) == 1:
343
- concated.index = orig_indices[0]
344
- return concated.drop(["i__"], axis=1)
345
-
346
- unconcated = ()
347
- for i in n_gdfs:
348
- gdf = concated[concated["i__"] == i]
349
- gdf.index = orig_indices[i]
350
- gdf = gdf.drop(["i__"], axis=1)
351
- unconcated = unconcated + (gdf,)
352
-
353
- return unconcated
354
-
355
-
356
359
  def get_overlapping_polygons(
357
360
  gdf: GeoDataFrame | GeoSeries, ignore_index: bool = False
358
361
  ) -> GeoDataFrame | GeoSeries:
@@ -457,21 +460,19 @@ def get_overlapping_polygon_product(gdf: GeoDataFrame | GeoSeries) -> pd.Index:
457
460
  return series
458
461
 
459
462
 
460
- def close_small_holes(
463
+ def close_all_holes(
461
464
  gdf: GeoDataFrame | GeoSeries,
462
- max_area: int | float,
463
465
  *,
466
+ without_islands: bool = True,
464
467
  copy: bool = True,
465
468
  ) -> GeoDataFrame | GeoSeries:
466
- """Closes holes in polygons if the area is less than the given maximum.
469
+ """Closes all holes in polygons.
467
470
 
468
471
  It takes a GeoDataFrame or GeoSeries of polygons and
469
- fills the holes that are smaller than the specified area given in units of
470
- either square meters ('max_m2') or square kilometers ('max_km2').
472
+ returns the outer circle.
471
473
 
472
474
  Args:
473
475
  gdf: GeoDataFrame or GeoSeries of polygons.
474
- max_area: The maximum area in the unit of the GeoDataFrame's crs.
475
476
  copy: if True (default), the input GeoDataFrame or GeoSeries is copied.
476
477
  Defaults to True.
477
478
 
@@ -479,17 +480,11 @@ def close_small_holes(
479
480
  A GeoDataFrame or GeoSeries of polygons with closed holes in the geometry
480
481
  column.
481
482
 
482
- Raises:
483
- ValueError: If the coordinate reference system of the GeoDataFrame is not in
484
- meter units.
485
- ValueError: If both 'max_m2' and 'max_km2' is given.
486
-
487
483
  Examples
488
484
  --------
489
-
490
485
  Let's create a circle with a hole in it.
491
486
 
492
- >>> from sgis import close_small_holes, buff, to_gdf
487
+ >>> from sgis import close_all_holes, buff, to_gdf
493
488
  >>> point = to_gdf([260000, 6650000], crs=25833)
494
489
  >>> point
495
490
  geometry
@@ -501,50 +496,54 @@ def close_small_holes(
501
496
  0 2.355807e+06
502
497
  dtype: float64
503
498
 
504
- Close holes smaller than 1 square kilometer (1 million square meters).
499
+ Close the hole.
505
500
 
506
- >>> holes_closed = close_small_holes(circle_with_hole, max_area=1_000_000)
501
+ >>> holes_closed = close_all_holes(circle_with_hole)
507
502
  >>> holes_closed.area
508
503
  0 3.141076e+06
509
504
  dtype: float64
510
-
511
- The hole will not be closed if it is larger.
512
-
513
- >>> holes_closed = close_small_holes(circle_with_hole, max_area=1_000)
514
- >>> holes_closed.area
515
- 0 2.355807e+06
516
- dtype: float64
517
505
  """
518
- if copy:
519
- gdf = gdf.copy()
520
-
521
- if isinstance(gdf, GeoDataFrame):
522
- gdf["geometry"] = gdf.geometry.map(
523
- lambda x: _close_small_holes_poly(x, max_area)
506
+ if not isinstance(gdf, (GeoDataFrame, GeoSeries)):
507
+ raise ValueError(
508
+ f"'gdf' should be of type GeoDataFrame or GeoSeries. Got {type(gdf)}"
524
509
  )
525
- return gdf
526
510
 
527
- elif isinstance(gdf, gpd.GeoSeries):
528
- return gdf.map(lambda x: _close_small_holes_poly(x, max_area))
511
+ if copy:
512
+ gdf = gdf.copy()
529
513
 
514
+ if without_islands:
515
+ all_geoms = gdf.unary_union
516
+ if isinstance(gdf, GeoDataFrame):
517
+ gdf["geometry"] = gdf.geometry.map(
518
+ lambda x: _close_all_holes_no_islands(x, all_geoms)
519
+ )
520
+ return gdf
521
+ else:
522
+ return gdf.map(lambda x: _close_all_holes_no_islands(x, all_geoms))
530
523
  else:
531
- raise ValueError(
532
- f"'gdf' should be of type GeoDataFrame or GeoSeries. Got {type(gdf)}"
533
- )
524
+ if isinstance(gdf, GeoDataFrame):
525
+ gdf["geometry"] = gdf.geometry.map(_close_all_holes)
526
+ return gdf
527
+ else:
528
+ return gdf.map(_close_all_holes)
534
529
 
535
530
 
536
- def close_all_holes(
531
+ def close_small_holes(
537
532
  gdf: GeoDataFrame | GeoSeries,
533
+ max_area: int | float,
538
534
  *,
535
+ without_islands: bool = True,
539
536
  copy: bool = True,
540
537
  ) -> GeoDataFrame | GeoSeries:
541
- """Closes all holes in polygons.
538
+ """Closes holes in polygons if the area is less than the given maximum.
542
539
 
543
540
  It takes a GeoDataFrame or GeoSeries of polygons and
544
- returns the outer circle.
541
+ fills the holes that are smaller than the specified area given in units of
542
+ either square meters ('max_m2') or square kilometers ('max_km2').
545
543
 
546
544
  Args:
547
545
  gdf: GeoDataFrame or GeoSeries of polygons.
546
+ max_area: The maximum area in the unit of the GeoDataFrame's crs.
548
547
  copy: if True (default), the input GeoDataFrame or GeoSeries is copied.
549
548
  Defaults to True.
550
549
 
@@ -552,11 +551,17 @@ def close_all_holes(
552
551
  A GeoDataFrame or GeoSeries of polygons with closed holes in the geometry
553
552
  column.
554
553
 
554
+ Raises:
555
+ ValueError: If the coordinate reference system of the GeoDataFrame is not in
556
+ meter units.
557
+ ValueError: If both 'max_m2' and 'max_km2' is given.
558
+
555
559
  Examples
556
560
  --------
561
+
557
562
  Let's create a circle with a hole in it.
558
563
 
559
- >>> from sgis import close_all_holes, buff, to_gdf
564
+ >>> from sgis import close_small_holes, buff, to_gdf
560
565
  >>> point = to_gdf([260000, 6650000], crs=25833)
561
566
  >>> point
562
567
  geometry
@@ -568,35 +573,51 @@ def close_all_holes(
568
573
  0 2.355807e+06
569
574
  dtype: float64
570
575
 
571
- Close the hole.
576
+ Close holes smaller than 1 square kilometer (1 million square meters).
572
577
 
573
- >>> holes_closed = close_all_holes(circle_with_hole)
578
+ >>> holes_closed = close_small_holes(circle_with_hole, max_area=1_000_000)
574
579
  >>> holes_closed.area
575
580
  0 3.141076e+06
576
581
  dtype: float64
577
- """
578
- if copy:
579
- gdf = gdf.copy()
580
582
 
581
- def close_all_holes_func(poly):
582
- return unary_union(polygons(get_exterior_ring(get_parts(poly))))
583
+ The hole will not be closed if it is larger.
583
584
 
584
- close_all_holes_func = np.vectorize(close_all_holes_func)
585
+ >>> holes_closed = close_small_holes(circle_with_hole, max_area=1_000)
586
+ >>> holes_closed.area
587
+ 0 2.355807e+06
588
+ dtype: float64
589
+ """
590
+ if not isinstance(gdf, (GeoSeries, GeoDataFrame)):
591
+ raise ValueError(
592
+ f"'gdf' should be of type GeoDataFrame or GeoSeries. Got {type(gdf)}"
593
+ )
585
594
 
586
- if isinstance(gdf, GeoDataFrame):
587
- gdf["geometry"] = close_all_holes_func(gdf.geometry)
588
- return gdf
595
+ if copy:
596
+ gdf = gdf.copy()
589
597
 
590
- elif isinstance(gdf, gpd.GeoSeries):
591
- return close_all_holes_func(gdf)
598
+ if without_islands:
599
+ all_geoms = gdf.unary_union
592
600
 
601
+ if isinstance(gdf, GeoDataFrame):
602
+ gdf["geometry"] = gdf.geometry.map(
603
+ lambda x: _close_small_holes_no_islands(x, max_area, all_geoms)
604
+ )
605
+ return gdf
606
+ else:
607
+ return gdf.map(
608
+ lambda x: _close_small_holes_no_islands(x, max_area, all_geoms)
609
+ )
593
610
  else:
594
- raise ValueError(
595
- f"'gdf' should be of type GeoDataFrame or GeoSeries. Got {type(gdf)}"
596
- )
611
+ if isinstance(gdf, GeoDataFrame):
612
+ gdf["geometry"] = gdf.geometry.map(
613
+ lambda x: _close_small_holes(x, max_area)
614
+ )
615
+ return gdf
616
+ else:
617
+ return gdf.map(lambda x: _close_small_holes(x, max_area))
597
618
 
598
619
 
599
- def _close_small_holes_poly(poly, max_area):
620
+ def _close_small_holes(poly, max_area):
600
621
  """Closes cmall holes within one shapely geometry of polygons."""
601
622
 
602
623
  # start with a list containing the polygon,
@@ -612,7 +633,56 @@ def _close_small_holes_poly(poly, max_area):
612
633
  for n in range(n_interior_rings):
613
634
  hole = polygons(get_interior_ring(part, n))
614
635
 
636
+ print(area(hole))
637
+
615
638
  if area(hole) < max_area:
616
639
  holes_closed.append(hole)
617
640
 
618
641
  return unary_union(holes_closed)
642
+
643
+
644
+ def _close_small_holes_no_islands(poly, max_area, all_geoms):
645
+ """Closes small holes within one shapely geometry of polygons."""
646
+
647
+ # start with a list containing the polygon,
648
+ # then append all holes smaller than 'max_km2' to the list.
649
+ holes_closed = [poly]
650
+ singlepart = get_parts(poly)
651
+ for part in singlepart:
652
+ n_interior_rings = get_num_interior_rings(part)
653
+
654
+ if not (n_interior_rings):
655
+ continue
656
+
657
+ for n in range(n_interior_rings):
658
+ hole = polygons(get_interior_ring(part, n))
659
+ no_islands = unary_union(hole.difference(all_geoms))
660
+ if area(no_islands) < max_area:
661
+ holes_closed.append(no_islands)
662
+
663
+ return unary_union(holes_closed)
664
+
665
+
666
+ def _close_all_holes(poly):
667
+ return unary_union(polygons(get_exterior_ring(get_parts(poly))))
668
+
669
+
670
+ def _close_all_holes_no_islands(poly, all_geoms):
671
+ """Closes all holes within one shapely geometry of polygons."""
672
+
673
+ # start with a list containing the polygon,
674
+ # then append all holes smaller than 'max_km2' to the list.
675
+ holes_closed = [poly]
676
+ singlepart = get_parts(poly)
677
+ for part in singlepart:
678
+ n_interior_rings = get_num_interior_rings(part)
679
+
680
+ if not (n_interior_rings):
681
+ continue
682
+
683
+ for n in range(n_interior_rings):
684
+ hole = polygons(get_interior_ring(part, n))
685
+ no_islands = unary_union(hole.difference(all_geoms))
686
+ holes_closed.append(no_islands)
687
+
688
+ return unary_union(holes_closed)
@@ -3,9 +3,10 @@ from collections.abc import Iterator, Sized
3
3
 
4
4
  import geopandas as gpd
5
5
  import pandas as pd
6
+ import shapely
6
7
  from geopandas import GeoDataFrame, GeoSeries
7
8
  from pandas.api.types import is_array_like, is_dict_like, is_list_like
8
- from shapely import Geometry, wkb, wkt
9
+ from shapely import Geometry, box, wkb, wkt
9
10
  from shapely.geometry import Point
10
11
  from shapely.ops import unary_union
11
12
 
@@ -128,7 +129,7 @@ def to_gdf(
128
129
  raise TypeError("'to_gdf' doesn't accept GeoDataFrames as input type.")
129
130
 
130
131
  if isinstance(geom, GeoSeries):
131
- geom_col = "geometry" if not geometry else geometry
132
+ geom_col = geometry if geometry else "geometry"
132
133
  return _geoseries_to_gdf(geom, geom_col, crs, **kwargs)
133
134
 
134
135
  geom_col = _find_geometry_column(geom, geometry)
@@ -144,8 +145,26 @@ def to_gdf(
144
145
  return GeoDataFrame({geom_col: geom}, geometry=geom_col, crs=crs, **kwargs)
145
146
 
146
147
  if not is_dict_like(geom):
147
- geom = GeoSeries(_make_shapely_geoms(geom), index=index)
148
- return GeoDataFrame({geom_col: geom}, geometry=geom_col, crs=crs, **kwargs)
148
+ if not hasattr(geom, "__iter__") and hasattr(geom, "__dict__"):
149
+ if all(attr in geom.__dict__ for attr in ["minx", "miny", "maxx", "maxy"]):
150
+ geom = GeoSeries(
151
+ shapely.box(*(geom.minx, geom.miny, geom.maxx, geom.maxy)),
152
+ index=index,
153
+ )
154
+ return GeoDataFrame(
155
+ {geom_col: geom}, geometry=geom_col, crs=crs, **kwargs
156
+ )
157
+ if hasattr(geom, "__iter__") and all(isinstance(g, dict) for g in geom):
158
+ crs = crs if crs else _get_crs(geom)
159
+ geom = pd.concat(GeoSeries(_from_json(g)) for g in geom)
160
+ if index is not None:
161
+ geom.index = index
162
+ else:
163
+ geom = geom.reset_index(drop=True)
164
+ return GeoDataFrame({geom_col: geom}, geometry=geom_col, crs=crs, **kwargs)
165
+ else:
166
+ geom = GeoSeries(_make_shapely_geoms(geom), index=index)
167
+ return GeoDataFrame({geom_col: geom}, geometry=geom_col, crs=crs, **kwargs)
149
168
 
150
169
  # now we have dict, Series or DataFrame
151
170
 
@@ -168,6 +187,8 @@ def to_gdf(
168
187
  geoseries = GeoSeries(
169
188
  _make_shapely_geoms(list(geom.values())[0]), index=index
170
189
  )
190
+ elif isinstance(geom, pd.Series):
191
+ geoseries = GeoSeries(_make_shapely_geoms(geom), index=index)
171
192
  else:
172
193
  geoseries = GeoSeries(_make_shapely_geoms(geom.iloc[:, 0]), index=index)
173
194
  return GeoDataFrame({key: geoseries}, geometry=key, crs=crs, **kwargs)
@@ -175,10 +196,61 @@ def to_gdf(
175
196
  if geometry and geom_col not in geom or isinstance(geom, pd.DataFrame):
176
197
  raise ValueError("Cannot find geometry column(s)", geometry)
177
198
 
199
+ # geojson, __geo_interface__
200
+ if (
201
+ isinstance(geom, dict)
202
+ and sum(key in geom for key in ["type", "coordinates", "features"]) >= 2
203
+ ):
204
+ if "geometry" in geom:
205
+ geometry = "geometry"
206
+
207
+ crs = crs if crs else _get_crs(geom)
208
+ print(crs)
209
+ geom = GeoSeries(_from_json(geom), index=index)
210
+ return GeoDataFrame({geom_col: geom}, geometry=geom_col, crs=crs, **kwargs)
211
+
178
212
  geoseries = _series_like_to_geoseries(geom, index=index)
179
213
  return GeoDataFrame(geometry=geoseries, crs=crs, **kwargs)
180
214
 
181
215
 
216
+ def _get_crs(geom):
217
+ if not is_dict_like(geom) and is_dict_like(geom[0]):
218
+ crss = list({_get_crs(g) for g in geom})
219
+ if len(crss) == 1:
220
+ return crss[0]
221
+ return None
222
+ if "properties" in geom:
223
+ return _get_crs(geom["properties"])
224
+ if "crs" in geom:
225
+ geom = geom["crs"]
226
+ while is_dict_like(geom):
227
+ if "properties" in geom:
228
+ geom = geom["properties"]
229
+ elif "name" in geom:
230
+ geom = geom["name"]
231
+ else:
232
+ return None
233
+ return geom
234
+ return None
235
+
236
+
237
+ def _from_json(geom: dict):
238
+ if not isinstance(geom, dict) and isinstance(geom[0], dict):
239
+ return [_from_json(g) for g in geom]
240
+ if "geometry" in geom:
241
+ return _from_json(geom["geometry"])
242
+ if "features" in geom:
243
+ return _from_json(geom["features"])
244
+ coords = geom["coordinates"]
245
+ constructor = eval("shapely.geometry." + geom.get("type", Point))
246
+ try:
247
+ return constructor(coords)
248
+ except TypeError:
249
+ while len(coords) == 1:
250
+ coords = coords[0]
251
+ return constructor(coords)
252
+
253
+
182
254
  def _series_like_to_geoseries(geom, index):
183
255
  if index is None:
184
256
  index = geom.keys()
@@ -250,6 +322,8 @@ def _is_one_geometry(geom) -> bool:
250
322
  def _make_shapely_geoms(geom):
251
323
  if _is_one_geometry(geom):
252
324
  return _make_one_shapely_geom(geom)
325
+ if isinstance(geom, dict) and "coordinates" in geom:
326
+ return _from_json(geom)
253
327
  return (_make_one_shapely_geom(g) for g in geom)
254
328
 
255
329
 
@@ -292,9 +366,11 @@ def _make_one_shapely_geom(geom):
292
366
  elif len(geom) == 2 or len(geom) == 3:
293
367
  return Point(geom)
294
368
 
369
+ elif len(geom) == 4:
370
+ return box(*geom)
295
371
  else:
296
372
  raise ValueError(
297
373
  "If 'geom' is an iterable, each item should consist of "
298
- "wkt, wkb or 2/3 coordinates (x, y, z). Got ",
374
+ "wkt, wkb or (x, y (z) or bbox). Got ",
299
375
  geom,
300
376
  )
sgis/helpers.py CHANGED
@@ -58,9 +58,9 @@ def get_name(var: object, n: int = 5) -> str | None:
58
58
  frame = inspect.currentframe().f_back.f_back
59
59
 
60
60
  for _ in range(n):
61
- locals_ = frame.f_locals
62
-
63
- names = [var_name for var_name, var_val in locals_.items() if var_val is var]
61
+ names = [
62
+ var_name for var_name, var_val in frame.f_locals.items() if var_val is var
63
+ ]
64
64
  if names and len(names) == 1:
65
65
  return names[0]
66
66
 
sgis/maps/examine.py ADDED
@@ -0,0 +1,192 @@
1
+ import geopandas as gpd
2
+
3
+ from ..helpers import unit_is_degrees
4
+ from .maps import clipmap
5
+
6
+
7
+ class Examine:
8
+ """Explore geometries one row at a time.
9
+
10
+ It takes one or more GeoDataFrames and shows an interactive map
11
+ of one area at the time with the 'next', 'prev' and 'current' methods.
12
+
13
+ After creating the examiner object, the 'next' method will create a map
14
+ showing all geometries within a given radius (the size parameter) of the
15
+ first geometry in 'mask_gdf' (or the first speficied gdf). The 'next' method
16
+ can then be repeated.
17
+
18
+ Args:
19
+ *gdfs: One or more GeoDataFrames. The rows of the first GeoDataFrame
20
+ will be used as masks, unless 'mask_gdf' is specified.
21
+ column: Column to use as colors.
22
+ mask_gdf: Optional GeoDataFrame to use as mask iterator. The geometries
23
+ of mask_gdf will not be shown.
24
+ size: Number of meters (or other crs unit) to buffer the mask geometry
25
+ before clipping.
26
+ sort_values: Optional sorting column(s) of the mask GeoDataFrame. Rows
27
+ will be iterated through from the top.
28
+ **kwargs: Additional keyword arguments passed to sgis.clipmap.
29
+
30
+ Examples
31
+ --------
32
+ Create the examiner.
33
+
34
+ >>> import sgis as sg
35
+ >>> roads = sg.read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/roads_oslo_2022.parquet")
36
+ >>> points = sg.read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/points_oslo.parquet")
37
+ >>> e = sg.Examine(points, roads)
38
+ >>> e
39
+
40
+ Then the line below can be repeated for all rows if 'points'. This has to be
41
+ in a separate notebook cell to the previous.
42
+
43
+ >>> e.next()
44
+
45
+ Previous geometry:
46
+
47
+ >>> e.prev()
48
+
49
+ Repeating the current area with another layer and new column:
50
+
51
+ >>> some_points = points.sample(100)
52
+ >>> e.current(some_points, column="idx")
53
+
54
+ The row number can also be specified manually.
55
+ Can be done in 'next', 'prev' and 'current'.
56
+
57
+ >>> e.next(i=101)
58
+
59
+ This will create an examiner where 'points' is not shown, only used as mask.
60
+
61
+ >>> e = sg.Examine(roads, mask_gdf=points, column="oneway")
62
+ """
63
+
64
+ def __init__(
65
+ self,
66
+ *gdfs: gpd.GeoDataFrame,
67
+ column: str | None = None,
68
+ mask_gdf: gpd.GeoDataFrame | None = None,
69
+ sort_values: str | None = None,
70
+ size: int | float = 1000,
71
+ **kwargs,
72
+ ):
73
+ if not all(isinstance(gdf, gpd.GeoDataFrame) for gdf in gdfs):
74
+ raise ValueError("gdfs must be of type GeoDataFrame.")
75
+
76
+ self.gdfs = gdfs
77
+ if mask_gdf is None:
78
+ self.mask_gdf = gdfs[0]
79
+ else:
80
+ self.mask_gdf = mask_gdf
81
+
82
+ if unit_is_degrees(self.mask_gdf) and size > 360:
83
+ raise ValueError(
84
+ "CRS unit is degrees. Use geopandas' "
85
+ "to_crs method to change crs to e.g. UTM. "
86
+ "Or set 'size' to a smaller number."
87
+ )
88
+
89
+ if sort_values is not None:
90
+ self.mask_gdf = self.mask_gdf.sort_values(sort_values)
91
+
92
+ self.indices = list(range(len(gdfs[0])))
93
+ self.i = 0
94
+ self.column = column
95
+ self.size = size
96
+ self.kwargs = kwargs
97
+
98
+ def next(self, *gdfs, i: int | None = None, **kwargs):
99
+ """Displays a map of geometries within the next row of the mask gdf.
100
+
101
+ Args:
102
+ *gdfs: Optional GeoDataFrames to be added on top of the current.
103
+ i: Optionally set the integer index of which row to use as mask.
104
+ **kwargs: Additional keyword arguments passed to sgis.clipmap.
105
+ """
106
+ gdfs = () if not gdfs else gdfs
107
+ self.gdfs = self.gdfs + gdfs
108
+ if kwargs:
109
+ kwargs = self._fix_kwargs(kwargs)
110
+ self.kwargs = self.kwargs | kwargs
111
+
112
+ if i:
113
+ self.i = i
114
+
115
+ if self.i >= len(self.mask_gdf):
116
+ print("All rows are shown.")
117
+ return
118
+
119
+ print(f"{self.i + 1} of {len(self.mask_gdf)}")
120
+ clipmap(
121
+ *self.gdfs,
122
+ self.column,
123
+ mask=self.mask_gdf.iloc[[self.i]].buffer(self.size),
124
+ **self.kwargs,
125
+ )
126
+ self.i += 1
127
+
128
+ def prev(self, *gdfs, i: int | None = None, **kwargs):
129
+ """Displays a map of geometries within the previus row of the mask gdf.
130
+
131
+ Args:
132
+ *gdfs: Optional GeoDataFrames to be added on top of the current.
133
+ i: Optionally set the integer index of which row to use as mask.
134
+ **kwargs: Additional keyword arguments passed to sgis.clipmap.
135
+ """
136
+ gdfs = () if not gdfs else gdfs
137
+ self.gdfs = self.gdfs + gdfs
138
+ if kwargs:
139
+ kwargs = self._fix_kwargs(kwargs)
140
+ self.kwargs = self.kwargs | kwargs
141
+
142
+ self.i -= 2
143
+
144
+ if i:
145
+ self.i = i
146
+
147
+ print(f"{self.i + 1} of {len(self.mask_gdf)}")
148
+ clipmap(
149
+ *self.gdfs,
150
+ self.column,
151
+ mask=self.mask_gdf.iloc[[self.i]].buffer(self.size),
152
+ **self.kwargs,
153
+ )
154
+
155
+ def current(self, *gdfs, i: int | None = None, **kwargs):
156
+ """Repeat the last shown map."""
157
+ gdfs = () if not gdfs else gdfs
158
+ self.gdfs = self.gdfs + gdfs
159
+ if kwargs:
160
+ kwargs = self._fix_kwargs(kwargs)
161
+ self.kwargs = self.kwargs | kwargs
162
+
163
+ if i:
164
+ self.i = i
165
+
166
+ print(f"{self.i + 1} of {len(self.mask_gdf)}")
167
+ clipmap(
168
+ *self.gdfs,
169
+ self.column,
170
+ mask=self.mask_gdf.iloc[[self.i]].buffer(self.size),
171
+ **self.kwargs,
172
+ )
173
+
174
+ def get_current_mask(self) -> gpd.GeoDataFrame:
175
+ """Returns a GeoDataFrame of the last shown mask geometry."""
176
+ return self.mask_gdf.iloc[[self.i]]
177
+
178
+ def get_current_geoms(self) -> tuple[gpd.GeoDataFrame]:
179
+ """Returns all GeoDataFrames in the area of the last shown mask geometry."""
180
+ mask = self.mask_gdf.iloc[[self.i]]
181
+ gdfs = ()
182
+ for gdf in self.gdfs:
183
+ gdfs = gdfs + (gdf.clip(mask.buffer(self.size)),)
184
+ return gdfs
185
+
186
+ def _fix_kwargs(self, kwargs) -> dict:
187
+ self.size = kwargs.pop("size", self.size)
188
+ self.column = kwargs.pop("column", self.column)
189
+ return kwargs
190
+
191
+ def __repr__(self) -> str:
192
+ return f"{self.__class__}(indices={len(self.indices)}, current={self.i}, n_gdfs={len(self.gdfs)})"
sgis/maps/explore.py CHANGED
@@ -4,6 +4,7 @@ This module holds the Explore class, which is the basis for the explore, samplem
4
4
  clipmap functions from the 'maps' module.
5
5
  """
6
6
  import warnings
7
+ from numbers import Number
7
8
  from statistics import mean
8
9
 
9
10
  import branca as bc
@@ -15,10 +16,11 @@ from folium import plugins
15
16
  from geopandas import GeoDataFrame
16
17
  from IPython.display import display
17
18
  from jinja2 import Template
19
+ from shapely import Geometry
18
20
  from shapely.geometry import LineString
19
21
 
20
22
  from ..geopandas_tools.general import clean_geoms, make_all_singlepart
21
- from ..geopandas_tools.geometry_types import get_geom_type
23
+ from ..geopandas_tools.geometry_types import get_geom_type, to_single_geom_type
22
24
  from ..geopandas_tools.to_geodataframe import to_gdf
23
25
  from ..helpers import unit_is_degrees
24
26
  from .httpserver import run_html_server
@@ -110,6 +112,17 @@ class Explore(Map):
110
112
 
111
113
  super().__init__(*gdfs, column=column, **kwargs)
112
114
 
115
+ # remove columns not renerable by leaflet (list columns etc.)
116
+ new_gdfs = []
117
+ for gdf in self.gdfs:
118
+ cols_to_keep = [
119
+ col
120
+ for col in gdf.columns
121
+ if isinstance(gdf[col].iloc[0], (Number, str, Geometry))
122
+ ]
123
+ new_gdfs.append(gdf[cols_to_keep])
124
+ self._gdfs = new_gdfs
125
+
113
126
  self.popup = popup
114
127
  self.max_zoom = max_zoom
115
128
  self.smooth_factor = smooth_factor
@@ -266,6 +279,7 @@ class Explore(Map):
266
279
 
267
280
  new_gdfs = []
268
281
  for gdf in self._gdfs:
282
+ print(gdf)
269
283
  if get_geom_type(gdf) == "mixed" and not unit_is_degrees(gdf):
270
284
  gdf[gdf._geometry_column_name] = gdf.buffer(0.01)
271
285
  gdf = make_all_singlepart(gdf)
@@ -615,7 +629,26 @@ class Explore(Map):
615
629
  "supported as marker values"
616
630
  )
617
631
 
618
- gdf = clean_geoms(gdf)
632
+ gdf = clean_geoms(gdf).pipe(make_all_singlepart)
633
+ if get_geom_type(gdf) == "mixed":
634
+ if gdf.geom_type.str.lower().str.contains("polygon").any():
635
+ warnings.warn(
636
+ "GeoJsonTooltip is not configured to render for GeoJson "
637
+ "GeometryCollection geometries. Keeping only polygons."
638
+ )
639
+ gdf = to_single_geom_type(gdf, geom_type="polygon")
640
+ elif gdf.geom_type.str.lower().str.contains("line").any():
641
+ warnings.warn(
642
+ "GeoJsonTooltip is not configured to render for GeoJson "
643
+ "GeometryCollection geometries. Keeping only lines."
644
+ )
645
+ gdf = to_single_geom_type(gdf, geom_type="line")
646
+ else:
647
+ warnings.warn(
648
+ "GeoJsonTooltip is not configured to render for GeoJson "
649
+ "GeometryCollection geometries. Keeping only points."
650
+ )
651
+ gdf = to_single_geom_type(gdf, geom_type="point")
619
652
 
620
653
  # prepare tooltip and popup
621
654
  if isinstance(gdf, GeoDataFrame):
sgis/maps/map.py CHANGED
@@ -467,6 +467,8 @@ class Map:
467
467
  if gdf[self._column].isna().all():
468
468
  return np.repeat(len(bins), len(gdf))
469
469
 
470
+ # need numpy.nan instead of pd.NA as of now
471
+ gdf[self._column] = gdf[self._column].fillna(np.nan)
470
472
  classified = np.searchsorted(bins, gdf[self._column])
471
473
 
472
474
  return classified
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ssb-sgis
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Summary: GIS functions used at Statistics Norway.
5
5
  Home-page: https://github.com/statisticsnorway/ssb-sgis
6
6
  License: MIT
@@ -1,21 +1,22 @@
1
- sgis/__init__.py,sha256=7XYr7wiJ4lw4lQDSj7B2JhLzREA_sgw3_CuqWaJf9b8,2334
1
+ sgis/__init__.py,sha256=84lCHfBsH9nHwopwoPTMNHs1FvTWemuRPLALAhUEFIA,2445
2
2
  sgis/dapla.py,sha256=BlJ62kLwpTTQtmbj0Yutbh-bwokVPXHVb3QsRlMugF8,3542
3
3
  sgis/exceptions.py,sha256=ztMp4sB9xxPvwj2IEsO5kOaB4FmHuU_7-M2pZ7qaxTs,576
4
4
  sgis/geopandas_tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- sgis/geopandas_tools/buffer_dissolve_explode.py,sha256=46uBomQJMDFtGWVNZWyZLl-NLMwYyhr4wWkwj6BTxoo,8080
6
- sgis/geopandas_tools/general.py,sha256=BGBXobFRFl9fmFF-vRkkrcA2xHKGy4hylOEC4y4Ooto,17659
5
+ sgis/geopandas_tools/buffer_dissolve_explode.py,sha256=JHFdhFVURHKAW9NraQZ7YEtvp_lBveGdL0PkBZn1fuE,8780
6
+ sgis/geopandas_tools/general.py,sha256=Hbk1A_aFH_xgD_TkvaJQDW1U2lJYh-LHI9CmsPTkHaI,17930
7
7
  sgis/geopandas_tools/geometry_types.py,sha256=o3MbBP-aI7hVWWKVr_5p91TDhjiqZ_2IGxJq7SxlCT4,5870
8
8
  sgis/geopandas_tools/neighbors.py,sha256=tv8bmYgq4VNFbXmT2wcmJsFH8946NwbIBMQXAi3n8L4,14520
9
9
  sgis/geopandas_tools/overlay.py,sha256=RSxrDF0sXs6ZMxbeBJC9HFBVM4yaz10-cdbq3SCosFQ,11862
10
10
  sgis/geopandas_tools/point_operations.py,sha256=3JynroucouAbpON4DWG32S3MQQGmfIJuY7D6gkqtk70,6888
11
- sgis/geopandas_tools/polygon_operations.py,sha256=PfdrYLMmPtmlvOc9TkwYabsXxGe8B25F_Vzc7Sp1WUM,20336
12
- sgis/geopandas_tools/to_geodataframe.py,sha256=4jOy0YvXBIiOEqQx7_ept5xfd39R1XKPN_OVK8kxhp8,9722
13
- sgis/helpers.py,sha256=OqTojkSl-JVKlJzqqB-d_0CH6mk7_LS1DkiIjp1gD8E,2674
11
+ sgis/geopandas_tools/polygon_operations.py,sha256=84AGRHKZR-3zKKVDNbKYCziC9YNUVm2qUEpfRvmIdn4,22635
12
+ sgis/geopandas_tools/to_geodataframe.py,sha256=qtbeiQ9rPcM9afopNPzlN2fiMH5o6SoIYDotX6e_E0Q,12458
13
+ sgis/helpers.py,sha256=6tElQx9Rr3nbQPp_vAgugw-pNHNuYQO-Ta2311_EcWs,2669
14
14
  sgis/maps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- sgis/maps/explore.py,sha256=mab8LE8c_dsGwS1gVCCA2A3r5_Vh7hnzEfEs5MBadvE,23631
15
+ sgis/maps/examine.py,sha256=Wf48WwyNyPC1vYdw-n7GpvJYhwd-YMd_VZLOUI5vWZ4,6479
16
+ sgis/maps/explore.py,sha256=d16h8hHyUeSfsrAWdeFUPK6XT6S7-xr1w4_cHu2XA_I,25138
16
17
  sgis/maps/httpserver.py,sha256=7ksCSs-WlchcREgjdCZd6II-riJpox34HpVXsCzN_AU,1923
17
18
  sgis/maps/legend.py,sha256=GXAqGOb_zAWcDavd5aHzRyRB7nTRhPCQfSupYA693lk,20499
18
- sgis/maps/map.py,sha256=9sqidnL_ERS0gkmXoEnOS-mAkqsT1kId_X2J2KG5tDU,18490
19
+ sgis/maps/map.py,sha256=niK6N0eFJjAalxjHTNA7kh-2KuazLVsnWlu8i9Ava7o,18611
19
20
  sgis/maps/maps.py,sha256=NaK_wu4RGf6kKRUnnY7gLtxAY9x0d6gKxgQLubDbgHY,15961
20
21
  sgis/maps/thematicmap.py,sha256=6aVPciftW1YjxjRVQDipxayl3aI3tHpYiZ3HfpnSavc,14132
21
22
  sgis/networkanalysis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -33,7 +34,7 @@ sgis/networkanalysis/networkanalysisrules.py,sha256=BhhaSXIyBRNzxSOUP2kVBIR--TRq
33
34
  sgis/networkanalysis/nodes.py,sha256=Ys3FjB39Pir3U0jOoLKIPxCC4psC9mdlqdC7G6dSJg0,6767
34
35
  sgis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
36
  sgis/read_parquet.py,sha256=GSW2NDy4-XosbamPEzB1xhWxFAPHuGEJZglfQ-V6DzY,3774
36
- ssb_sgis-0.2.6.dist-info/LICENSE,sha256=lL2h0dNKGTKAE0CjTy62SDbRennVD1xPgM5LzGqhKeo,1074
37
- ssb_sgis-0.2.6.dist-info/METADATA,sha256=elNsqzxjoSHPYPuH4fBF1Ims1Qb_qm0a4nxACdH_q7g,8831
38
- ssb_sgis-0.2.6.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
39
- ssb_sgis-0.2.6.dist-info/RECORD,,
37
+ ssb_sgis-0.2.8.dist-info/LICENSE,sha256=lL2h0dNKGTKAE0CjTy62SDbRennVD1xPgM5LzGqhKeo,1074
38
+ ssb_sgis-0.2.8.dist-info/METADATA,sha256=hR3yvnhqhsaE8jEvutRwNHAFJq78Hhua5JodgYX99mE,8831
39
+ ssb_sgis-0.2.8.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
40
+ ssb_sgis-0.2.8.dist-info/RECORD,,