ssb-sgis 1.0.3__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 (39) hide show
  1. sgis/__init__.py +10 -3
  2. sgis/debug_config.py +24 -0
  3. sgis/geopandas_tools/bounds.py +16 -21
  4. sgis/geopandas_tools/buffer_dissolve_explode.py +112 -30
  5. sgis/geopandas_tools/centerlines.py +4 -91
  6. sgis/geopandas_tools/cleaning.py +1576 -583
  7. sgis/geopandas_tools/conversion.py +24 -14
  8. sgis/geopandas_tools/duplicates.py +27 -6
  9. sgis/geopandas_tools/general.py +259 -100
  10. sgis/geopandas_tools/geometry_types.py +1 -1
  11. sgis/geopandas_tools/neighbors.py +16 -12
  12. sgis/geopandas_tools/overlay.py +2 -2
  13. sgis/geopandas_tools/point_operations.py +3 -3
  14. sgis/geopandas_tools/polygon_operations.py +505 -100
  15. sgis/geopandas_tools/polygons_as_rings.py +40 -8
  16. sgis/geopandas_tools/sfilter.py +26 -9
  17. sgis/io/dapla_functions.py +238 -19
  18. sgis/maps/examine.py +11 -10
  19. sgis/maps/explore.py +227 -155
  20. sgis/maps/legend.py +13 -4
  21. sgis/maps/map.py +22 -13
  22. sgis/maps/maps.py +100 -29
  23. sgis/maps/thematicmap.py +25 -18
  24. sgis/networkanalysis/_service_area.py +6 -1
  25. sgis/networkanalysis/cutting_lines.py +12 -5
  26. sgis/networkanalysis/finding_isolated_networks.py +13 -6
  27. sgis/networkanalysis/networkanalysis.py +10 -12
  28. sgis/parallel/parallel.py +27 -10
  29. sgis/raster/base.py +208 -0
  30. sgis/raster/cube.py +3 -3
  31. sgis/raster/image_collection.py +1419 -722
  32. sgis/raster/indices.py +10 -7
  33. sgis/raster/raster.py +7 -7
  34. sgis/raster/sentinel_config.py +33 -17
  35. {ssb_sgis-1.0.3.dist-info → ssb_sgis-1.0.4.dist-info}/METADATA +6 -7
  36. ssb_sgis-1.0.4.dist-info/RECORD +62 -0
  37. ssb_sgis-1.0.3.dist-info/RECORD +0 -61
  38. {ssb_sgis-1.0.3.dist-info → ssb_sgis-1.0.4.dist-info}/LICENSE +0 -0
  39. {ssb_sgis-1.0.3.dist-info → ssb_sgis-1.0.4.dist-info}/WHEEL +0 -0
sgis/maps/maps.py CHANGED
@@ -19,6 +19,7 @@ from shapely import Geometry
19
19
  from shapely import box
20
20
  from shapely.geometry import Polygon
21
21
 
22
+ from ..debug_config import _NoExplore
22
23
  from ..geopandas_tools.bounds import get_total_bounds
23
24
  from ..geopandas_tools.conversion import to_bbox
24
25
  from ..geopandas_tools.conversion import to_gdf
@@ -84,7 +85,8 @@ def explore(
84
85
  browser: bool = False,
85
86
  smooth_factor: int | float = 1.5,
86
87
  size: int | None = None,
87
- max_images: int = 15,
88
+ max_images: int = 10,
89
+ images_to_gdf: bool = False,
88
90
  **kwargs,
89
91
  ) -> Explore:
90
92
  """Interactive map of GeoDataFrames with layers that can be toggled on/off.
@@ -113,7 +115,9 @@ def explore(
113
115
  size: The buffer distance. Only used when center is given. It then defaults to
114
116
  1000.
115
117
  max_images: Maximum number of images (Image, ImageCollection, Band) to show per
116
- map. Defaults to 15.
118
+ map. Defaults to 10.
119
+ images_to_gdf: If True (not default), images (Image, ImageCollection, Band)
120
+ will be converted to GeoDataFrame and added to the map.
117
121
  **kwargs: Keyword arguments to pass to geopandas.GeoDataFrame.explore, for
118
122
  instance 'cmap' to change the colors, 'scheme' to change how the data
119
123
  is grouped. This defaults to 'fisherjenkssampled' for numeric data.
@@ -143,6 +147,9 @@ def explore(
143
147
  >>> points["meters"] = points.length
144
148
  >>> sg.explore(roads, points, column="meters", cmap="plasma", max_zoom=60, center_4326=(10.7463, 59.92, 500))
145
149
  """
150
+ if isinstance(center, _NoExplore):
151
+ return
152
+
146
153
  gdfs, column, kwargs = Map._separate_args(gdfs, column, kwargs)
147
154
 
148
155
  loc_mask, kwargs = _get_location_mask(kwargs | {"size": size}, gdfs)
@@ -186,13 +193,63 @@ def explore(
186
193
  mask = to_gdf(center, crs=from_crs)
187
194
 
188
195
  bounds: Polygon = box(*get_total_bounds(*gdfs, *list(kwargs.values())))
189
- if not mask.intersects(bounds).any():
190
- mask = mask.set_crs(4326, allow_override=True)
191
196
 
192
- try:
193
- mask = mask.to_crs(to_crs)
194
- except ValueError:
195
- pass
197
+ any_intersections: bool = mask.intersects(bounds).any()
198
+ if not any_intersections and to_crs is None:
199
+ mask = to_gdf(Polygon(), to_crs)
200
+ elif not any_intersections:
201
+ bounds4326 = to_gdf(bounds, to_crs).to_crs(25833).geometry.iloc[0]
202
+ mask4326 = mask.set_crs(4326, allow_override=True).to_crs(25833)
203
+
204
+ if (mask4326.distance(bounds4326) > size).all():
205
+ # try flipping coordinates
206
+ x, y = next(iter(mask.geometry.iloc[0].coords))
207
+ mask4326 = to_gdf([y, x], 4326).to_crs(25833)
208
+
209
+ if (mask4326.distance(bounds4326) > size).all():
210
+ mask = to_gdf(Polygon(), to_crs)
211
+ else:
212
+ mask = mask4326.to_crs(to_crs)
213
+
214
+ # else:
215
+ # mask_flipped = mask
216
+
217
+ # # coords = mask.get_coordinates()
218
+ # if (
219
+ # (mask_flipped.distance(bounds) > size).all()
220
+ # # and coords["x"].max() < 180
221
+ # # and coords["y"].max() < 180
222
+ # # and coords["x"].min() > -180
223
+ # # and coords["y"].min() > -180
224
+ # ):
225
+ # try:
226
+ # bounds4326 = to_gdf(bounds, to_crs).to_crs(4326).geometry.iloc[0]
227
+ # except ValueError:
228
+ # bounds4326 = to_gdf(bounds, to_crs).set_crs(4326).geometry.iloc[0]
229
+
230
+ # mask4326 = mask.set_crs(4326, allow_override=True)
231
+
232
+ # if (mask4326.distance(bounds4326) > size).all():
233
+ # # try flipping coordinates
234
+ # x, y = list(mask4326.geometry.iloc[0].coords)[0]
235
+ # mask4326 = to_gdf([y, x], 4326)
236
+
237
+ # mask = mask4326
238
+
239
+ # # if mask4326.intersects(bounds4326).any():
240
+ # # mask = mask4326
241
+ # # else:
242
+ # # try:
243
+ # # mask = mask.to_crs(to_crs)
244
+ # # except ValueError:
245
+ # # pass
246
+ # else:
247
+ # mask = mask_flipped
248
+
249
+ # try:
250
+ # mask = mask.to_crs(to_crs)
251
+ # except ValueError:
252
+ # pass
196
253
 
197
254
  if get_geom_type(mask) in ["point", "line"]:
198
255
  mask = mask.buffer(size)
@@ -236,7 +293,7 @@ def samplemap(
236
293
  smooth_factor: int = 1.5,
237
294
  explore: bool = True,
238
295
  browser: bool = False,
239
- max_images: int = 15,
296
+ max_images: int = 10,
240
297
  **kwargs,
241
298
  ) -> Explore:
242
299
  """Shows an interactive map of a random area of GeoDataFrames.
@@ -269,7 +326,7 @@ def samplemap(
269
326
  browser: If False (default), the maps will be shown in Jupyter.
270
327
  If True the maps will be opened in a browser folder.
271
328
  max_images: Maximum number of images (Image, ImageCollection, Band) to show per
272
- map. Defaults to 15.
329
+ map. Defaults to 10.
273
330
  **kwargs: Keyword arguments to pass to geopandas.GeoDataFrame.explore, for
274
331
  instance 'cmap' to change the colors, 'scheme' to change how the data
275
332
  is grouped. This defaults to 'fisherjenkssampled' for numeric data.
@@ -294,6 +351,9 @@ def samplemap(
294
351
  >>> samplemap(roads, points, size=5_000, column="meters")
295
352
 
296
353
  """
354
+ if isinstance(kwargs.get("center", None), _NoExplore):
355
+ return
356
+
297
357
  if gdfs and len(gdfs) > 1 and isinstance(gdfs[-1], (float, int)):
298
358
  *gdfs, size = gdfs
299
359
 
@@ -311,7 +371,7 @@ def samplemap(
311
371
 
312
372
  if mask is None:
313
373
  try:
314
- sample = sample.geometry.dropna().sample(1)
374
+ sample = sample.geometry.loc[lambda x: ~x.is_empty].sample(1)
315
375
  except Exception:
316
376
  try:
317
377
  sample = sample.sample(1)
@@ -327,11 +387,16 @@ def samplemap(
327
387
  except Exception:
328
388
  sample = to_gdf(to_shapely(to_bbox(sample))).explode(ignore_index=True)
329
389
 
330
- sample = sample.clip(mask).sample(1)
390
+ sample = sample.clip(mask).explode(ignore_index=True).sample(1)
331
391
 
392
+ print(locals())
332
393
  random_point = sample.sample_points(size=1)
333
394
 
334
- center = (random_point.geometry.iloc[0].x, random_point.geometry.iloc[0].y)
395
+ try:
396
+ center = (random_point.geometry.iloc[0].x, random_point.geometry.iloc[0].y)
397
+ except AttributeError as e:
398
+ raise AttributeError(e, random_point.geometry.iloc[0]) from e
399
+
335
400
  print(f"center={center}, size={size}")
336
401
 
337
402
  mask = random_point.buffer(size)
@@ -357,7 +422,7 @@ def clipmap(
357
422
  max_zoom: int = 40,
358
423
  smooth_factor: int | float = 1.5,
359
424
  browser: bool = False,
360
- max_images: int = 15,
425
+ max_images: int = 10,
361
426
  **kwargs,
362
427
  ) -> Explore | Map:
363
428
  """Shows an interactive map of a of GeoDataFrames clipped to the mask extent.
@@ -385,7 +450,7 @@ def clipmap(
385
450
  browser: If False (default), the maps will be shown in Jupyter.
386
451
  If True the maps will be opened in a browser folder.
387
452
  max_images: Maximum number of images (Image, ImageCollection, Band) to show per
388
- map. Defaults to 15.
453
+ map. Defaults to 10.
389
454
  **kwargs: Keyword arguments to pass to geopandas.GeoDataFrame.explore, for
390
455
  instance 'cmap' to change the colors, 'scheme' to change how the data
391
456
  is grouped. This defaults to 'fisherjenkssampled' for numeric data.
@@ -395,8 +460,10 @@ def clipmap(
395
460
  explore: same functionality, but shows the entire area of the geometries.
396
461
  samplemap: same functionality, but shows only a random area of a given size.
397
462
  """
398
- gdfs, column, kwargs = Map._separate_args(gdfs, column, kwargs)
463
+ if isinstance(kwargs.get("center", None), _NoExplore):
464
+ return
399
465
 
466
+ gdfs, column, kwargs = Map._separate_args(gdfs, column, kwargs)
400
467
  if mask is None and len(gdfs) > 1:
401
468
  mask = gdfs[-1]
402
469
  gdfs = gdfs[:-1]
@@ -450,7 +517,9 @@ def clipmap(
450
517
  return m
451
518
 
452
519
 
453
- def explore_locals(*gdfs: GeoDataFrame, convert: bool = True, **kwargs) -> None:
520
+ def explore_locals(
521
+ *gdfs: GeoDataFrame, convert: bool = True, crs: Any | None = None, **kwargs
522
+ ) -> None:
454
523
  """Displays all local variables with geometries (GeoDataFrame etc.).
455
524
 
456
525
  Local means inside a function or file/notebook.
@@ -459,8 +528,11 @@ def explore_locals(*gdfs: GeoDataFrame, convert: bool = True, **kwargs) -> None:
459
528
  *gdfs: Additional GeoDataFrames.
460
529
  convert: If True (default), non-GeoDataFrames will be converted
461
530
  to GeoDataFrames if possible.
531
+ crs: Optional crs if no objects have any crs.
462
532
  **kwargs: keyword arguments passed to sg.explore.
463
533
  """
534
+ if isinstance(kwargs.get("center", None), _NoExplore):
535
+ return
464
536
 
465
537
  def as_dict(obj):
466
538
  if hasattr(obj, "__dict__"):
@@ -482,11 +554,19 @@ def explore_locals(*gdfs: GeoDataFrame, convert: bool = True, **kwargs) -> None:
482
554
  if not convert:
483
555
  continue
484
556
 
557
+ try:
558
+ gdf = clean_geoms(to_gdf(value, crs=crs))
559
+ if len(gdf):
560
+ local_gdfs[name] = gdf
561
+ continue
562
+ except Exception:
563
+ pass
564
+
485
565
  if isinstance(value, dict) or hasattr(value, "__dict__"):
486
566
  # add dicts or classes with GeoDataFrames to kwargs
487
567
  for key, val in as_dict(value).items():
488
568
  if isinstance(val, allowed_types):
489
- gdf = clean_geoms(to_gdf(val))
569
+ gdf = clean_geoms(to_gdf(val, crs=crs))
490
570
  if len(gdf):
491
571
  local_gdfs[key] = gdf
492
572
 
@@ -494,22 +574,13 @@ def explore_locals(*gdfs: GeoDataFrame, convert: bool = True, **kwargs) -> None:
494
574
  try:
495
575
  for k, v in val.items():
496
576
  if isinstance(v, allowed_types):
497
- gdf = clean_geoms(to_gdf(v))
577
+ gdf = clean_geoms(to_gdf(v, crs=crs))
498
578
  if len(gdf):
499
579
  local_gdfs[k] = gdf
500
580
  except Exception:
501
581
  # no need to raise here
502
582
  pass
503
583
 
504
- continue
505
- try:
506
- gdf = clean_geoms(to_gdf(value))
507
- if len(gdf):
508
- local_gdfs[name] = gdf
509
- continue
510
- except Exception:
511
- pass
512
-
513
584
  if local_gdfs:
514
585
  break
515
586
 
@@ -518,7 +589,7 @@ def explore_locals(*gdfs: GeoDataFrame, convert: bool = True, **kwargs) -> None:
518
589
  if not frame:
519
590
  break
520
591
 
521
- return explore(*gdfs, **local_gdfs, **kwargs)
592
+ return explore(*gdfs, **(local_gdfs | kwargs))
522
593
 
523
594
 
524
595
  def qtm(
sgis/maps/thematicmap.py CHANGED
@@ -68,24 +68,24 @@ class ThematicMap(Map):
68
68
  legend_kwargs: dictionary with attributes for the legend. E.g.:
69
69
  title: Legend title. Defaults to the column name.
70
70
  rounding: If positive number, it will round floats to n decimals.
71
- If negative, eg. -2, the number 3429 is rounded to 3400.
72
- By default, the rounding depends on the column's maximum value
73
- and standard deviation.
71
+ If negative, eg. -2, the number 3429 is rounded to 3400.
72
+ By default, the rounding depends on the column's maximum value
73
+ and standard deviation.
74
74
  position: The legend's x and y position in the plot. By default, it's
75
- decided dynamically by finding the space with most distance to
76
- the geometries. To be specified as a tuple of
77
- x and y position between 0 and 1. E.g. position=(0.8, 0.2) for a position
78
- in the bottom right corner, (0.2, 0.8) for the upper left corner.
75
+ decided dynamically by finding the space with most distance to
76
+ the geometries. To be specified as a tuple of
77
+ x and y position between 0 and 1. E.g. position=(0.8, 0.2) for a position
78
+ in the bottom right corner, (0.2, 0.8) for the upper left corner.
79
79
  pretty_labels: Whether to capitalize words in text categories.
80
80
  label_suffix: For numeric columns. The text to put after each number
81
- in the legend labels. Defaults to None.
81
+ in the legend labels. Defaults to None.
82
82
  label_sep: For numeric columns. Text to put in between the two numbers
83
- in each color group in the legend. Defaults to '-'.
83
+ in each color group in the legend. Defaults to '-'.
84
84
  thousand_sep: For numeric columns. Separator between each thousand for
85
- large numbers. Defaults to None, meaning no separator.
85
+ large numbers. Defaults to None, meaning no separator.
86
86
  decimal_mark: For numeric columns. Text to use as decimal point.
87
- Defaults to None, meaning '.' (dot) unless 'thousand_sep' is
88
- '.'. In this case, ',' (comma) will be used as decimal mark.
87
+ Defaults to None, meaning '.' (dot) unless 'thousand_sep' is
88
+ '.'. In this case, ',' (comma) will be used as decimal mark.
89
89
  **kwargs: Additional attributes for the map. E.g.:
90
90
  title_color (str): Color of the title font.
91
91
  title_fontsize (int): Color of the title font.
@@ -96,7 +96,7 @@ class ThematicMap(Map):
96
96
  nan_color: Color for missing data.
97
97
 
98
98
  Examples:
99
- --------
99
+ ---------
100
100
  >>> import sgis as sg
101
101
  >>> points = sg.random_points(100, loc=1000).pipe(sg.buff, np.random.rand(100) * 100)
102
102
  >>> points2 = sg.random_points(100, loc=1000).pipe(sg.buff, np.random.rand(100) * 100)
@@ -166,6 +166,7 @@ class ThematicMap(Map):
166
166
  nan_label: str = "Missing",
167
167
  legend_kwargs: dict | None = None,
168
168
  title_kwargs: dict | None = None,
169
+ legend: bool = False,
169
170
  **kwargs,
170
171
  ) -> None:
171
172
  """Initialiser."""
@@ -178,6 +179,9 @@ class ThematicMap(Map):
178
179
  nan_label=nan_label,
179
180
  )
180
181
 
182
+ if not legend:
183
+ self.legend = None
184
+
181
185
  self.title = title
182
186
  self._size = size
183
187
  self._dark = dark
@@ -239,10 +243,11 @@ class ThematicMap(Map):
239
243
  raise TypeError(
240
244
  f"{self.__class__.__name__} legend_kwargs got an unexpected key {key}"
241
245
  )
242
- try:
243
- setattr(self.legend, key, value)
244
- except Exception:
245
- setattr(self.legend, f"_{key}", value)
246
+ if self.legend is not None:
247
+ try:
248
+ setattr(self.legend, key, value)
249
+ except Exception:
250
+ setattr(self.legend, f"_{key}", value)
246
251
 
247
252
  @property
248
253
  def valid_keywords(self) -> set[str]:
@@ -398,7 +403,7 @@ class ThematicMap(Map):
398
403
  return kwargs
399
404
 
400
405
  else:
401
- if self.legend.rounding and self.legend.rounding < 0:
406
+ if self.legend and self.legend.rounding and self.legend.rounding < 0:
402
407
  self.bins = prettify_bins(self.bins, self.legend.rounding)
403
408
  self.bins = list({round(bin_, 5) for bin_ in self.bins})
404
409
  self.bins.sort()
@@ -463,6 +468,8 @@ class ThematicMap(Map):
463
468
 
464
469
  def _create_legend(self) -> None:
465
470
  """Instantiate the Legend class."""
471
+ if self.legend is None:
472
+ return
466
473
  kwargs = {}
467
474
  if self._dark:
468
475
  kwargs["facecolor"] = "#0f0f0f"
@@ -5,6 +5,7 @@ from igraph import Graph
5
5
  from shapely import force_2d
6
6
  from shapely import reverse
7
7
  from shapely import unary_union
8
+ from shapely import union_all
8
9
  from shapely.geometry import MultiPoint
9
10
  from shapely.geometry import Point
10
11
  from shapely.ops import nearest_points
@@ -113,7 +114,11 @@ def _service_area(
113
114
  else:
114
115
  snapped_origin: Point = nearest_points(
115
116
  nodes_union,
116
- origins.loc[origins["temp_idx"] == idx, "geometry"].unary_union,
117
+ union_all(
118
+ origins.loc[
119
+ origins["temp_idx"] == idx, "geometry"
120
+ ].geometry.values
121
+ ),
117
122
  )[0]
118
123
 
119
124
  within = sfilter(within, snapped_origin.buffer(0.01))
@@ -77,7 +77,7 @@ def split_lines_by_nearest_point(
77
77
  """
78
78
  PRECISION = 1e-6
79
79
 
80
- if not len(gdf):
80
+ if not len(gdf) or not len(points):
81
81
  return gdf
82
82
 
83
83
  if (points.crs is not None and gdf.crs is not None) and not points.crs.equals(
@@ -86,10 +86,14 @@ def split_lines_by_nearest_point(
86
86
  raise ValueError("crs mismatch:", points.crs, "and", gdf.crs)
87
87
 
88
88
  if get_geom_type(gdf) != "line":
89
- raise ValueError("'gdf' should only have line geometries.", gdf.geom_type)
89
+ raise ValueError(
90
+ f"'gdf' should only have line geometriess. Got {gdf.geom_type.value_counts()}"
91
+ )
90
92
 
91
93
  if get_geom_type(points) != "point":
92
- raise ValueError("'points' should only have point geometries.")
94
+ raise ValueError(
95
+ f"'points' should only have point geometries. Got {points.geom_type.value_counts()}"
96
+ )
93
97
 
94
98
  gdf = gdf.copy()
95
99
 
@@ -230,9 +234,12 @@ def _change_line_endpoint(
230
234
  .values
231
235
  )
232
236
 
233
- relevant_lines_mapped = relevant_lines.groupby(level=0)["geometry"].agg(LineString)
237
+ is_line = relevant_lines.groupby(level=0).size() > 1
238
+ relevant_lines_mapped = (
239
+ relevant_lines.loc[is_line].groupby(level=0)["geometry"].agg(LineString)
240
+ )
234
241
 
235
- gdf.loc[is_relevant, "geometry"] = relevant_lines_mapped
242
+ gdf.loc[relevant_lines_mapped.index, "geometry"] = relevant_lines_mapped
236
243
 
237
244
  return gdf
238
245
 
@@ -1,6 +1,7 @@
1
1
  """Functions for Finding network components in a GeoDataFrame of lines."""
2
2
 
3
3
  import networkx as nx
4
+ import pandas as pd
4
5
  from geopandas import GeoDataFrame
5
6
 
6
7
  from .nodes import make_node_ids
@@ -81,7 +82,7 @@ def get_component_size(gdf: GeoDataFrame) -> GeoDataFrame:
81
82
  >>> roads = read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/roads_oslo_2022.parquet")
82
83
 
83
84
  >>> roads = get_component_size(roads)
84
- >>> roads.component_size.value_counts().head()
85
+ >>> roads["component_size"].value_counts().head()
85
86
  component_size
86
87
  79180 85638
87
88
  2 1601
@@ -101,11 +102,17 @@ def get_component_size(gdf: GeoDataFrame) -> GeoDataFrame:
101
102
  graph.add_edges_from(edges)
102
103
  components = [list(x) for x in nx.connected_components(graph)]
103
104
 
104
- componentsdict = {
105
- idx: len(component) for component in components for idx in component
106
- }
107
-
108
- gdf["component_size"] = gdf.source.map(componentsdict)
105
+ mapper = pd.DataFrame(
106
+ {
107
+ idx: [i, len(component)]
108
+ for i, component in enumerate(components)
109
+ for idx in component
110
+ },
111
+ ).transpose()
112
+ mapper.columns = ["component_index", "component_size"]
113
+
114
+ gdf["component_index"] = gdf["source"].map(mapper["component_index"])
115
+ gdf["component_size"] = gdf["source"].map(mapper["component_size"])
109
116
 
110
117
  gdf = gdf.drop(
111
118
  ["source_wkt", "target_wkt", "source", "target", "n_source", "n_target"], axis=1
@@ -1027,7 +1027,7 @@ class NetworkAnalysis:
1027
1027
 
1028
1028
  if dissolve:
1029
1029
  results = results.dissolve(by=["origin", self.rules.weight]).loc[
1030
- :, ["geometry"]
1030
+ :, [results.geometry.name]
1031
1031
  ]
1032
1032
 
1033
1033
  results = results.reset_index()
@@ -1038,7 +1038,7 @@ class NetworkAnalysis:
1038
1038
  ].rename(columns={"temp_idx": "origin"})[["origin"]]
1039
1039
 
1040
1040
  if len(missing):
1041
- missing["geometry"] = pd.NA
1041
+ missing[results.geometry.name] = pd.NA
1042
1042
  results = pd.concat([results, missing], ignore_index=True)
1043
1043
 
1044
1044
  results["origin"] = results["origin"].map(self.origins.idx_dict)
@@ -1151,7 +1151,7 @@ class NetworkAnalysis:
1151
1151
  if not all(results.geometry.isna()):
1152
1152
  if dissolve:
1153
1153
  results = results.dissolve(by=["origin", self.rules.weight]).loc[
1154
- :, ["geometry"]
1154
+ :, [results.geometry.name]
1155
1155
  ]
1156
1156
  else:
1157
1157
  results = results.dissolve(
@@ -1166,7 +1166,7 @@ class NetworkAnalysis:
1166
1166
  ].rename(columns={"temp_idx": "origin"})[["origin"]]
1167
1167
 
1168
1168
  if len(missing):
1169
- missing["geometry"] = pd.NA
1169
+ missing[results.geometry.name] = pd.NA
1170
1170
  results = pd.concat([results, missing], ignore_index=True)
1171
1171
 
1172
1172
  results["origin"] = results["origin"].map(self.origins.idx_dict)
@@ -1329,7 +1329,7 @@ class NetworkAnalysis:
1329
1329
  df["cost_std"] = results[self.rules.weight].std()
1330
1330
 
1331
1331
  if fun == "service_area":
1332
- df["percent_missing"] = results["geometry"].isna().mean() * 100
1332
+ df["percent_missing"] = results[results.geometry.name].isna().mean() * 100
1333
1333
  else:
1334
1334
  df["destinations_count"] = len(self.destinations.gdf)
1335
1335
 
@@ -1456,7 +1456,7 @@ class NetworkAnalysis:
1456
1456
  else:
1457
1457
  points = self.origins.gdf
1458
1458
 
1459
- points = points.drop_duplicates("geometry")
1459
+ points = points.drop_duplicates(points.geometry.name)
1460
1460
 
1461
1461
  self.network.gdf["meters_"] = self.network.gdf.length
1462
1462
 
@@ -1573,7 +1573,7 @@ class NetworkAnalysis:
1573
1573
  This method is best stored in the NetworkAnalysis class,
1574
1574
  since the point classes are instantiated each time an analysis is run.
1575
1575
  """
1576
- if self.wkts[what] != [geom.wkt for geom in points.geometry]:
1576
+ if not np.array_equal(self.wkts[what], points.geometry.to_wkt().values):
1577
1577
  return True
1578
1578
 
1579
1579
  if not all(x in self.graph.vs["name"] for x in list(points.temp_idx.values)):
@@ -1590,17 +1590,15 @@ class NetworkAnalysis:
1590
1590
  """
1591
1591
  self.wkts = {}
1592
1592
 
1593
- self.wkts["network"] = [geom.wkt for geom in self.network.gdf.geometry]
1593
+ self.wkts["network"] = self.network.gdf.geometry.to_wkt().values
1594
1594
 
1595
1595
  if not hasattr(self, "origins"):
1596
1596
  return
1597
1597
 
1598
- self.wkts["origins"] = [geom.wkt for geom in self.origins.gdf.geometry]
1598
+ self.wkts["origins"] = self.origins.gdf.geometry.to_wkt().values
1599
1599
 
1600
1600
  if self.destinations is not None:
1601
- self.wkts["destinations"] = [
1602
- geom.wkt for geom in self.destinations.gdf.geometry
1603
- ]
1601
+ self.wkts["destinations"] = self.destinations.gdf.geometry.to_wkt().values
1604
1602
 
1605
1603
  @staticmethod
1606
1604
  def _sort_breaks(breaks: str | list | tuple | int | float) -> list[float | int]:
sgis/parallel/parallel.py CHANGED
@@ -32,7 +32,6 @@ try:
32
32
  from ..io.dapla_functions import read_geopandas
33
33
  from ..io.dapla_functions import write_geopandas
34
34
 
35
- # from ..io.write_municipality_data import write_municipality_data
36
35
  except ImportError:
37
36
  pass
38
37
 
@@ -40,11 +39,8 @@ except ImportError:
40
39
  try:
41
40
  from dapla import read_pandas
42
41
  from dapla import write_pandas
43
- from dapla.gcs import GCSFileSystem
44
42
  except ImportError:
45
-
46
- class GCSFileSystem:
47
- """Placeholder."""
43
+ pass
48
44
 
49
45
 
50
46
  class Parallel:
@@ -806,7 +802,11 @@ def _write_one_muni(
806
802
 
807
803
  if not len(gdf_muni):
808
804
  if write_empty:
809
- gdf_muni = gdf_muni.drop(columns="geometry", errors="ignore")
805
+ try:
806
+ geom_col = gdf.geometry.name
807
+ except AttributeError:
808
+ geom_col = "geometry"
809
+ gdf_muni = gdf_muni.drop(columns=geom_col, errors="ignore")
810
810
  gdf_muni["geometry"] = None
811
811
  write_pandas(gdf_muni, out)
812
812
  return
@@ -834,7 +834,11 @@ def _write_one_muni_with_neighbors(
834
834
 
835
835
  if not len(gdf_neighbor):
836
836
  if write_empty:
837
- gdf_neighbor = gdf_neighbor.drop(columns="geometry", errors="ignore")
837
+ try:
838
+ geom_col = gdf.geometry.name
839
+ except AttributeError:
840
+ geom_col = "geometry"
841
+ gdf_neighbor = gdf_neighbor.drop(columns=geom_col, errors="ignore")
838
842
  gdf_neighbor["geometry"] = None
839
843
  write_pandas(gdf_neighbor, out)
840
844
  return
@@ -880,7 +884,9 @@ def _fix_missing_muni_numbers(
880
884
  )
881
885
 
882
886
  try:
883
- municipalities = municipalities[[muni_number_col, "geometry"]].to_crs(gdf.crs)
887
+ municipalities = municipalities[
888
+ [muni_number_col, municipalities.geometry.name]
889
+ ].to_crs(gdf.crs)
884
890
  except Exception as e:
885
891
  raise e.__class__(e, to_print) from e
886
892
 
@@ -967,10 +973,21 @@ def parallel_overlay(
967
973
 
968
974
 
969
975
  def _clean_intersection(
970
- df1: GeoDataFrame, df2: GeoDataFrame, to_print: str = ""
976
+ df1: GeoDataFrame, df2: GeoDataFrame, to_print: str | None = None
971
977
  ) -> GeoDataFrame:
972
978
  print(to_print, "- intersection chunk len:", len(df1))
973
- return clean_overlay(df1, df2, how="intersection")
979
+ cols_to_keep = df1.columns.union(df2.columns.difference({df2.geometry.name}))
980
+ df1["_range_idx"] = range(len(df1))
981
+ joined = df1.sjoin(df2, predicate="within", how="left")
982
+ within = joined.loc[joined["_range_idx"].notna(), cols_to_keep]
983
+ not_within = joined.loc[joined["_range_idx"].isna(), df1.columns]
984
+ return pd.concat(
985
+ [
986
+ within,
987
+ clean_overlay(not_within, df2, how="intersection"),
988
+ ],
989
+ ignore_index=True,
990
+ )
974
991
 
975
992
 
976
993
  def chunkwise(