ssb-sgis 1.0.4__py3-none-any.whl → 1.0.6__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.
@@ -11,6 +11,7 @@ import geopandas as gpd
11
11
  import joblib
12
12
  import pandas as pd
13
13
  import pyarrow
14
+ import pyarrow.parquet as pq
14
15
  import shapely
15
16
  from geopandas import GeoDataFrame
16
17
  from geopandas import GeoSeries
@@ -18,6 +19,7 @@ from geopandas.io.arrow import _geopandas_to_arrow
18
19
  from pandas import DataFrame
19
20
  from pyarrow import ArrowInvalid
20
21
 
22
+ from ..geopandas_tools.general import get_common_crs
21
23
  from ..geopandas_tools.sfilter import sfilter
22
24
 
23
25
  PANDAS_FALLBACK_INFO = " Set pandas_fallback=True to ignore this error."
@@ -63,6 +65,7 @@ def read_geopandas(
63
65
  if not isinstance(gcs_path, (str | Path | os.PathLike)):
64
66
  kwargs |= {"file_system": file_system, "pandas_fallback": pandas_fallback}
65
67
 
68
+ cols = {}
66
69
  if mask is not None:
67
70
  if not isinstance(gcs_path, GeoSeries):
68
71
  bounds_series: GeoSeries = get_bounds_series(
@@ -95,14 +98,25 @@ def read_geopandas(
95
98
  paths = list(gcs_path)
96
99
 
97
100
  if threads is None:
98
- threads = min(len(gcs_path), int(multiprocessing.cpu_count())) or 1
101
+ threads = min(len(paths), int(multiprocessing.cpu_count())) or 1
99
102
 
100
103
  # recursive read with threads
101
104
  with joblib.Parallel(n_jobs=threads, backend="threading") as parallel:
102
105
  dfs: list[GeoDataFrame] = parallel(
103
106
  joblib.delayed(read_geopandas)(x, **kwargs) for x in paths
104
107
  )
105
- df = pd.concat(dfs)
108
+
109
+ if dfs:
110
+ df = pd.concat(dfs, ignore_index=True)
111
+ try:
112
+ df = GeoDataFrame(df)
113
+ except Exception as e:
114
+ if not pandas_fallback:
115
+ print(e)
116
+ raise e
117
+ else:
118
+ df = GeoDataFrame(cols | {"geometry": []})
119
+
106
120
  if mask is not None:
107
121
  return sfilter(df, mask)
108
122
  return df
@@ -131,6 +145,8 @@ def read_geopandas(
131
145
  raise e.__class__(
132
146
  f"{e.__class__.__name__}: {e} for {df}." + more_txt
133
147
  ) from e
148
+ except Exception as e:
149
+ raise e.__class__(f"{e.__class__.__name__}: {e} for {gcs_path}.") from e
134
150
 
135
151
  else:
136
152
  with file_system.open(gcs_path, mode="rb") as file:
@@ -148,6 +164,10 @@ def read_geopandas(
148
164
  raise e.__class__(
149
165
  f"{e.__class__.__name__}: {e} for {df}. " + more_txt
150
166
  ) from e
167
+ except Exception as e:
168
+ raise e.__class__(
169
+ f"{e.__class__.__name__}: {e} for {df}." + more_txt
170
+ ) from e
151
171
 
152
172
  if mask is not None:
153
173
  return sfilter(df, mask)
@@ -159,14 +179,14 @@ def _get_bounds_parquet(
159
179
  ) -> tuple[list[float], dict] | tuple[None, None]:
160
180
  with file_system.open(path) as f:
161
181
  try:
162
- num_rows = pyarrow.parquet.read_metadata(f).num_rows
182
+ num_rows = pq.read_metadata(f).num_rows
163
183
  except ArrowInvalid as e:
164
184
  if not file_system.isfile(f):
165
185
  return None, None
166
186
  raise ArrowInvalid(e, path) from e
167
187
  if not num_rows:
168
188
  return None, None
169
- meta = pyarrow.parquet.read_schema(f).metadata
189
+ meta = pq.read_schema(f).metadata
170
190
  try:
171
191
  meta = json.loads(meta[b"geo"])["columns"]["geometry"]
172
192
  except KeyError as e:
@@ -182,7 +202,7 @@ def _get_bounds_parquet(
182
202
 
183
203
  def _get_columns(path: str | Path, file_system: dp.gcs.GCSFileSystem) -> pd.Index:
184
204
  with file_system.open(path) as f:
185
- schema = pyarrow.parquet.read_schema(f)
205
+ schema = pq.read_schema(f)
186
206
  index_cols = _get_index_cols(schema)
187
207
  return pd.Index(schema.names).difference(index_cols)
188
208
 
@@ -266,17 +286,13 @@ def get_bounds_series(
266
286
  for path in paths
267
287
  )
268
288
  crss = {json.dumps(x[1]) for x in bounds}
269
- crss = {
270
- crs
271
- for crs in crss
272
- if not any(str(crs).lower() == txt for txt in ["none", "null"])
273
- }
274
- if not crss:
275
- crs = None
276
- elif len(crss) == 1:
277
- crs = next(iter(crss))
278
- else:
279
- raise ValueError(f"crs mismatch: {crss}")
289
+ crs = get_common_crs(
290
+ [
291
+ crs
292
+ for crs in crss
293
+ if not any(str(crs).lower() == txt for txt in ["none", "null"])
294
+ ]
295
+ )
280
296
  return GeoSeries(
281
297
  [shapely.box(*bbox[0]) if bbox[0] is not None else None for bbox in bounds],
282
298
  index=paths,
@@ -355,7 +371,7 @@ def write_geopandas(
355
371
  schema_version=None,
356
372
  write_covering_bbox=write_covering_bbox,
357
373
  )
358
- pyarrow.parquet.write_table(table, buffer, compression="snappy", **kwargs)
374
+ pq.write_table(table, buffer, compression="snappy", **kwargs)
359
375
  return
360
376
 
361
377
  layer = kwargs.pop("layer", None)
sgis/maps/explore.py CHANGED
@@ -22,6 +22,7 @@ import matplotlib
22
22
  import matplotlib.pyplot as plt
23
23
  import numpy as np
24
24
  import pandas as pd
25
+ import shapely
25
26
  import xyzservices
26
27
  from folium import plugins
27
28
  from geopandas import GeoDataFrame
@@ -219,7 +220,7 @@ class Explore(Map):
219
220
  "OpenStreetMap",
220
221
  "dark",
221
222
  "norge_i_bilder",
222
- "grunnkart",
223
+ # "grunnkart",
223
224
  )
224
225
 
225
226
  def __init__(
@@ -464,9 +465,9 @@ class Explore(Map):
464
465
  pass
465
466
 
466
467
  try:
467
- sample = to_gdf(to_shapely(sample)).explode(ignore_index=True)
468
+ sample = to_gdf(to_shapely(sample)).pipe(make_all_singlepart)
468
469
  except Exception:
469
- sample = to_gdf(to_shapely(to_bbox(sample))).explode(ignore_index=True)
470
+ sample = to_gdf(to_shapely(to_bbox(sample))).pipe(make_all_singlepart)
470
471
 
471
472
  random_point = sample.sample_points(size=1)
472
473
 
@@ -666,7 +667,12 @@ class Explore(Map):
666
667
  if not len(gdf):
667
668
  continue
668
669
 
669
- gdf = self._to_single_geom_type(gdf)
670
+ gdf = self._to_single_geom_type(make_all_singlepart(gdf))
671
+
672
+ if not len(gdf):
673
+ continue
674
+
675
+ gdf.geometry = shapely.force_2d(gdf.geometry.values)
670
676
  gdf = self._prepare_gdf_for_map(gdf)
671
677
 
672
678
  gjs = self._make_geojson(
@@ -737,7 +743,12 @@ class Explore(Map):
737
743
  if not len(gdf):
738
744
  continue
739
745
 
740
- gdf = self._to_single_geom_type(gdf)
746
+ gdf = self._to_single_geom_type(make_all_singlepart(gdf))
747
+
748
+ if not len(gdf):
749
+ continue
750
+
751
+ gdf.geometry = shapely.force_2d(gdf.geometry.values)
741
752
  gdf = self._prepare_gdf_for_map(gdf)
742
753
 
743
754
  classified = self._classify_from_bins(gdf, bins=self.bins)
sgis/maps/map.py CHANGED
@@ -16,6 +16,7 @@ from geopandas import GeoDataFrame
16
16
  from geopandas import GeoSeries
17
17
  from jenkspy import jenks_breaks
18
18
  from mapclassify import classify
19
+ from pandas.errors import PerformanceWarning
19
20
  from shapely import Geometry
20
21
 
21
22
  from ..geopandas_tools.conversion import to_gdf
@@ -41,6 +42,8 @@ except ImportError:
41
42
  warnings.filterwarnings(
42
43
  action="ignore", category=matplotlib.MatplotlibDeprecationWarning
43
44
  )
45
+ warnings.filterwarnings(action="ignore", category=PerformanceWarning)
46
+
44
47
  pd.options.mode.chained_assignment = None
45
48
 
46
49
 
sgis/maps/maps.py CHANGED
@@ -389,7 +389,6 @@ def samplemap(
389
389
 
390
390
  sample = sample.clip(mask).explode(ignore_index=True).sample(1)
391
391
 
392
- print(locals())
393
392
  random_point = sample.sample_points(size=1)
394
393
 
395
394
  try:
@@ -3,17 +3,78 @@
3
3
  import geopandas as gpd
4
4
  import numpy as np
5
5
  import pandas as pd
6
+ import shapely
6
7
  from geopandas import GeoDataFrame
7
8
  from geopandas import GeoSeries
8
9
  from pandas import DataFrame
9
- from shapely import shortest_line
10
10
 
11
11
  from ..geopandas_tools.conversion import coordinate_array
12
+ from ..geopandas_tools.general import get_line_segments
12
13
  from ..geopandas_tools.neighbors import k_nearest_neighbors
14
+ from ..geopandas_tools.sfilter import sfilter
13
15
  from .nodes import make_edge_wkt_cols
14
16
  from .nodes import make_node_ids
15
17
 
16
18
 
19
+ def get_k_nearest_points_for_deadends(
20
+ lines: GeoDataFrame, k: int, max_distance: int
21
+ ) -> GeoDataFrame:
22
+
23
+ assert lines.index.is_unique
24
+ lines = lines.assign(_range_idx_left=range(len(lines)))
25
+ points = (
26
+ lines.assign(
27
+ geometry=lambda x: x.extract_unique_points().values,
28
+ _range_idx_right=range(len(lines)),
29
+ )
30
+ .explode(index_parts=False)
31
+ .sort_index()
32
+ )
33
+
34
+ points_grouper = points.groupby("_range_idx_right")["geometry"]
35
+ nodes = pd.concat(
36
+ [
37
+ points_grouper.nth(0),
38
+ points_grouper.nth(-1),
39
+ ]
40
+ )
41
+ nodes.index.name = "_range_idx_right"
42
+ nodes = nodes.reset_index()
43
+
44
+ def has_no_duplicates(nodes):
45
+ counts = nodes.geometry.value_counts()
46
+ return nodes.geometry.isin(counts[counts == 1].index)
47
+
48
+ deadends = nodes[has_no_duplicates].reset_index(drop=True)
49
+
50
+ deadends_buffered = deadends.assign(geometry=lambda x: x.buffer(max_distance))
51
+
52
+ segs_by_deadends = (
53
+ sfilter(lines, deadends_buffered)
54
+ .pipe(get_line_segments)
55
+ .sjoin(deadends_buffered)
56
+ .loc[lambda x: x["_range_idx_left"] != x["_range_idx_right"]]
57
+ )
58
+
59
+ lines_between = shapely.shortest_line(
60
+ segs_by_deadends.geometry.values,
61
+ deadends.loc[segs_by_deadends["index_right"].values].geometry.values,
62
+ )
63
+ segs_by_deadends.geometry.loc[:] = shapely.get_point(lines_between, 0)
64
+
65
+ length_mapper = dict(enumerate(shapely.length(lines_between)))
66
+ sorted_lengths = dict(
67
+ reversed(sorted(length_mapper.items(), key=lambda item: item[1]))
68
+ )
69
+ nearest_first = segs_by_deadends.iloc[list(sorted_lengths)]
70
+
71
+ k_nearest_per_deadend = nearest_first.geometry.groupby(level=0).apply(
72
+ lambda x: x.head(k)
73
+ )
74
+
75
+ return GeoDataFrame({"geometry": k_nearest_per_deadend.values}, crs=lines.crs)
76
+
77
+
17
78
  def close_network_holes(
18
79
  gdf: GeoDataFrame,
19
80
  max_distance: int | float,
@@ -94,13 +155,22 @@ def close_network_holes(
94
155
  lines, nodes = make_node_ids(gdf)
95
156
 
96
157
  # remove duplicates of lines going both directions
97
- lines["sorted"] = [
98
- "_".join(sorted([s, t]))
99
- for s, t in zip(lines["source"], lines["target"], strict=True)
158
+ lines["_sorted"] = [
159
+ "_".join(sorted([source, target])) + str(round(length, 4))
160
+ for source, target, length in zip(
161
+ lines["source"], lines["target"], lines.length, strict=True
162
+ )
100
163
  ]
101
164
 
165
+ lines = lines.drop_duplicates("_sorted").drop(columns="_sorted")
166
+
167
+ # new_lines, angles = _close_holes_all_lines(
102
168
  new_lines: GeoSeries = _close_holes_all_lines(
103
- lines.drop_duplicates("sorted"), nodes, max_distance, max_angle, idx_start=1
169
+ lines,
170
+ nodes,
171
+ max_distance,
172
+ max_angle,
173
+ idx_start=1,
104
174
  )
105
175
 
106
176
  new_lines = gpd.GeoDataFrame(
@@ -128,15 +198,6 @@ def close_network_holes(
128
198
  return pd.concat([lines, new_lines], ignore_index=True)
129
199
 
130
200
 
131
- def get_angle(array_a: np.ndarray, array_b: np.ndarray) -> np.ndarray:
132
- dx = array_b[:, 0] - array_a[:, 0]
133
- dy = array_b[:, 1] - array_a[:, 1]
134
-
135
- angles_rad = np.arctan2(dx, dy)
136
- angles_degrees = np.degrees(angles_rad)
137
- return angles_degrees
138
-
139
-
140
201
  def close_network_holes_to_deadends(
141
202
  gdf: GeoDataFrame,
142
203
  max_distance: int | float,
@@ -221,7 +282,7 @@ def _close_holes_all_lines(
221
282
  ) -> GeoSeries:
222
283
  k = min(len(nodes), 50)
223
284
 
224
- # make point gdf for the deadends and the other endpoint of the deadend lines
285
+ # make points for the deadends and the other endpoint of the deadend lines
225
286
  deadends_target = lines.loc[lines["n_target"] == 1].rename(
226
287
  columns={"target_wkt": "wkt", "source_wkt": "wkt_other_end"}
227
288
  )
@@ -251,6 +312,7 @@ def _close_holes_all_lines(
251
312
  # and endpoints of the new lines in lists, looping through the k neighbour points
252
313
  new_sources: list[str] = []
253
314
  new_targets: list[str] = []
315
+ # all_angles = []
254
316
  for i in np.arange(idx_start, k):
255
317
  # to break out of the loop if no new_targets that meet the condition are found
256
318
  len_now = len(new_sources)
@@ -270,8 +332,11 @@ def _close_holes_all_lines(
270
332
  deadends_other_end_array, deadends_array
271
333
  )
272
334
 
273
- angles_difference = np.abs(
274
- np.abs(angles_deadend_to_deadend_other_end) - np.abs(angles_deadend_to_node)
335
+ def get_angle_difference(angle1, angle2):
336
+ return np.abs((angle1 - angle2 + 180) % 360 - 180)
337
+
338
+ angles_difference = get_angle_difference(
339
+ angles_deadend_to_deadend_other_end, angles_deadend_to_node
275
340
  )
276
341
 
277
342
  angles_difference[
@@ -284,14 +349,18 @@ def _close_holes_all_lines(
284
349
  to_idx = indices[condition]
285
350
  to_wkt = nodes.iloc[to_idx]["wkt"]
286
351
 
352
+ # all_angles = all_angles + [
353
+ # diff
354
+ # for f, diff in zip(from_wkt, angles_difference[condition], strict=True)
355
+ # if f not in new_sources
356
+ # ]
357
+
287
358
  # now add the wkts to the lists of new sources and targets. If the source
288
359
  # is already added, the new wks will not be added again
289
360
  new_targets = new_targets + [
290
361
  t for f, t in zip(from_wkt, to_wkt, strict=True) if f not in new_sources
291
362
  ]
292
- new_sources = new_sources + [
293
- f for f, _ in zip(from_wkt, to_wkt, strict=True) if f not in new_sources
294
- ]
363
+ new_sources = new_sources + [f for f in from_wkt if f not in new_sources]
295
364
 
296
365
  # break out of the loop when no new new_targets meet the condition
297
366
  if len_now == len(new_sources):
@@ -300,7 +369,16 @@ def _close_holes_all_lines(
300
369
  # make GeoSeries with straight lines
301
370
  new_sources = gpd.GeoSeries.from_wkt(new_sources, crs=lines.crs)
302
371
  new_targets = gpd.GeoSeries.from_wkt(new_targets, crs=lines.crs)
303
- return shortest_line(new_sources, new_targets)
372
+ return shapely.shortest_line(new_sources, new_targets) # , all_angles
373
+
374
+
375
+ def get_angle(array_a: np.ndarray, array_b: np.ndarray) -> np.ndarray:
376
+ dx = array_b[:, 0] - array_a[:, 0]
377
+ dy = array_b[:, 1] - array_a[:, 1]
378
+
379
+ angles_rad = np.arctan2(dx, dy)
380
+ angles_degrees = np.degrees(angles_rad)
381
+ return angles_degrees
304
382
 
305
383
 
306
384
  def _find_holes_deadends(
@@ -347,7 +425,7 @@ def _find_holes_deadends(
347
425
  to_geom = deadends.loc[to_idx, "geometry"].reset_index(drop=True)
348
426
 
349
427
  # GeoDataFrame with straight lines
350
- new_lines = shortest_line(from_geom, to_geom)
428
+ new_lines = shapely.shortest_line(from_geom, to_geom)
351
429
  new_lines = gpd.GeoDataFrame({"geometry": new_lines}, geometry="geometry", crs=crs)
352
430
 
353
431
  return new_lines
@@ -7,19 +7,16 @@ import pandas as pd
7
7
  from geopandas import GeoDataFrame
8
8
  from pandas import DataFrame
9
9
  from pandas import Series
10
- from shapely import extract_unique_points
11
10
  from shapely import force_2d
12
11
  from shapely.geometry import LineString
13
12
  from shapely.geometry import Point
14
13
 
15
- from ..geopandas_tools.buffer_dissolve_explode import buff
16
- from ..geopandas_tools.conversion import to_gdf
14
+ from ..geopandas_tools.general import _split_lines_by_points_along_line
17
15
  from ..geopandas_tools.geometry_types import get_geom_type
18
- from ..geopandas_tools.neighbors import get_k_nearest_neighbors
19
16
  from ..geopandas_tools.point_operations import snap_all
20
17
  from ..geopandas_tools.point_operations import snap_within_distance
21
- from ..geopandas_tools.sfilter import sfilter_split
22
- from .nodes import make_edge_coords_cols
18
+
19
+ PRECISION = 1e-6
23
20
 
24
21
 
25
22
  def split_lines_by_nearest_point(
@@ -75,8 +72,6 @@ def split_lines_by_nearest_point(
75
72
  Not all lines were split. That is because some points were closest to an endpoint
76
73
  of a line.
77
74
  """
78
- PRECISION = 1e-6
79
-
80
75
  if not len(gdf) or not len(points):
81
76
  return gdf
82
77
 
@@ -103,145 +98,7 @@ def split_lines_by_nearest_point(
103
98
  else:
104
99
  snapped = snap_all(points, gdf)
105
100
 
106
- # find the lines that were snapped to (or are very close because of float rounding)
107
- snapped_buff = buff(snapped, PRECISION, resolution=16)
108
- relevant_lines, the_other_lines = sfilter_split(gdf, snapped_buff)
109
-
110
- if max_distance and not len(relevant_lines):
111
- if splitted_col:
112
- return gdf.assign(**{splitted_col: 1})
113
- return gdf
114
-
115
- # need consistent coordinate dimensions later
116
- # (doing it down here to not overwrite the original data)
117
- relevant_lines.geometry = force_2d(relevant_lines.geometry)
118
- snapped.geometry = force_2d(snapped.geometry)
119
-
120
- # split the lines with buffer + difference, since shaply.split usually doesn't work
121
- # relevant_lines["_idx"] = range(len(relevant_lines))
122
- splitted = relevant_lines.overlay(snapped_buff, how="difference").explode(
123
- ignore_index=True
124
- )
125
-
126
- # linearrings (maybe coded as linestrings) that were not split,
127
- # do not have edges and must be added in the end
128
- boundaries = splitted.geometry.boundary
129
- circles = splitted[boundaries.is_empty]
130
- splitted = splitted[~boundaries.is_empty]
131
-
132
- if not len(splitted):
133
- return pd.concat([the_other_lines, circles], ignore_index=True)
134
-
135
- # the endpoints of the new lines are now sligtly off. Using get_k_nearest_neighbors
136
- # to get the exact snapped point coordinates, . This will map the sligtly
137
- # wrong line endpoints with the point the line was split by.
138
-
139
- snapped["point_coords"] = [(geom.x, geom.y) for geom in snapped.geometry]
140
-
141
- # get line endpoints as columns (source_coords and target_coords)
142
- splitted = make_edge_coords_cols(splitted)
143
-
144
- splitted_source = to_gdf(splitted["source_coords"], crs=gdf.crs)
145
- splitted_target = to_gdf(splitted["target_coords"], crs=gdf.crs)
146
-
147
- def get_nearest(splitted: GeoDataFrame, snapped: GeoDataFrame) -> pd.DataFrame:
148
- """Find the nearest snapped point for each source and target of the lines."""
149
- return get_k_nearest_neighbors(splitted, snapped, k=1).loc[
150
- lambda x: x["distance"] <= PRECISION * 2
151
- ]
152
-
153
- # snapped = snapped.set_index("point_coords")
154
- snapped.index = snapped.geometry
155
- dists_source = get_nearest(splitted_source, snapped)
156
- dists_target = get_nearest(splitted_target, snapped)
157
-
158
- # neighbor_index: point coordinates as tuple
159
- pointmapper_source: pd.Series = dists_source["neighbor_index"]
160
- pointmapper_target: pd.Series = dists_target["neighbor_index"]
161
-
162
- # now, we can replace the source/target coordinate with the coordinates of
163
- # the snapped points.
164
-
165
- splitted = _change_line_endpoint(
166
- splitted,
167
- indices=dists_source.index,
168
- pointmapper=pointmapper_source,
169
- change_what="first",
170
- ) # i=0)
171
-
172
- # same for the lines where the target was split, but change the last coordinate
173
- splitted = _change_line_endpoint(
174
- splitted,
175
- indices=dists_target.index,
176
- pointmapper=pointmapper_target,
177
- change_what="last",
178
- ) # , i=-1)
179
-
180
- if splitted_col:
181
- splitted[splitted_col] = 1
182
-
183
- return pd.concat([the_other_lines, splitted, circles], ignore_index=True).drop(
184
- ["source_coords", "target_coords"], axis=1
185
- )
186
-
187
-
188
- def _change_line_endpoint(
189
- gdf: GeoDataFrame,
190
- indices: pd.Index,
191
- pointmapper: pd.Series,
192
- change_what: str | int,
193
- ) -> GeoDataFrame:
194
- """Modify the endpoints of selected lines in a GeoDataFrame based on an index mapping.
195
-
196
- This function updates the geometry of specified line features within a GeoDataFrame,
197
- changing either the first or last point of each line to new coordinates provided by a mapping.
198
- It is typically used in scenarios where line endpoints need to be adjusted to new locations,
199
- such as in network adjustments or data corrections.
200
-
201
- Args:
202
- gdf: A GeoDataFrame containing line geometries.
203
- indices: An Index object identifying the rows in the GeoDataFrame whose endpoints will be changed.
204
- pointmapper: A Series mapping from the index of lines to new point geometries.
205
- change_what: Specifies which endpoint of the line to change. Accepts 'first' or 0 for the
206
- starting point, and 'last' or -1 for the ending point.
207
-
208
- Returns:
209
- A GeoDataFrame with the specified line endpoints updated according to the pointmapper.
210
-
211
- Raises:
212
- ValueError: If `change_what` is not one of the accepted values ('first', 'last', 0, -1).
213
- """
214
- assert gdf.index.is_unique
215
-
216
- if change_what == "first" or change_what == 0:
217
- keep = "first"
218
- elif change_what == "last" or change_what == -1:
219
- keep = "last"
220
- else:
221
- raise ValueError(
222
- f"change_what should be 'first' or 'last' or 0 or -1. Got {change_what}"
223
- )
224
-
225
- is_relevant = gdf.index.isin(indices)
226
- relevant_lines = gdf.loc[is_relevant]
227
-
228
- relevant_lines.geometry = extract_unique_points(relevant_lines.geometry)
229
- relevant_lines = relevant_lines.explode(index_parts=False)
230
-
231
- relevant_lines.loc[lambda x: ~x.index.duplicated(keep=keep), "geometry"] = (
232
- relevant_lines.loc[lambda x: ~x.index.duplicated(keep=keep)]
233
- .index.map(pointmapper)
234
- .values
235
- )
236
-
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
- )
241
-
242
- gdf.loc[relevant_lines_mapped.index, "geometry"] = relevant_lines_mapped
243
-
244
- return gdf
101
+ return _split_lines_by_points_along_line(gdf, snapped, splitted_col=splitted_col)
245
102
 
246
103
 
247
104
  def cut_lines(
@@ -91,6 +91,11 @@ def get_component_size(gdf: GeoDataFrame) -> GeoDataFrame:
91
91
  3 346
92
92
  Name: count, dtype: int64
93
93
  """
94
+ if not len(gdf):
95
+ gdf["component_index"] = None
96
+ gdf["component_size"] = None
97
+ return gdf
98
+
94
99
  gdf, _ = make_node_ids(gdf)
95
100
 
96
101
  edges = [
@@ -109,6 +114,7 @@ def get_component_size(gdf: GeoDataFrame) -> GeoDataFrame:
109
114
  for idx in component
110
115
  },
111
116
  ).transpose()
117
+
112
118
  mapper.columns = ["component_index", "component_size"]
113
119
 
114
120
  gdf["component_index"] = gdf["source"].map(mapper["component_index"])