6
6
7
7
import geopandas as gpd
8
8
import numpy as np
9
+ import pandas as pd
9
10
import shapely
10
11
from libpysal import graph
11
12
from scipy import spatial
@@ -122,7 +123,7 @@ def voronoi_skeleton(
122
123
buffer : None | float | int = None
123
124
Optional custom buffer distance for dealing with Voronoi infinity issues.
124
125
secondary_snap_to : None | gpd.GeoSeries = None
125
- .. .
126
+ Fall-back series of geometries that shall be connected to the skeleton .
126
127
limit_distance : None | int = 2
127
128
...
128
129
consolidation_tolerance : None | float = None
@@ -236,7 +237,7 @@ def voronoi_skeleton(
236
237
if edgelines .shape [0 ] > 0 :
237
238
# if there is no explicit snapping target, snap to the boundary of the polygon
238
239
# via the shortest line. That is by definition always within the polygon
239
- # (Martin thinks)
240
+ # (Martin thinks) (James concurs)
240
241
if snap_to is not False :
241
242
if snap_to is None :
242
243
sl = shapely .shortest_line (
@@ -296,24 +297,45 @@ def _consolidate(
296
297
return edgelines
297
298
298
299
299
- def snap_to_targets (edgelines , poly , snap_to , secondary_snap_to = None ):
300
+ def snap_to_targets (
301
+ edgelines : np .ndarray ,
302
+ poly : shapely .Polygon ,
303
+ snap_to : gpd .GeoSeries ,
304
+ secondary_snap_to : None | gpd .GeoSeries = None ,
305
+ ) -> tuple [list [shapely .LineString ], list [shapely .Point ]]:
306
+ """Snap edgelines to vertices.
307
+
308
+ Parameters
309
+ ----------
310
+ edgelines : numpy.ndarray
311
+ Voronoi skeleton edges.
312
+ poly : None | shapely.Polygon = None
313
+ Polygon enclosed by ``lines``.
314
+ snap_to : None | gpd.GeoSeries = None
315
+ Series of geometries that shall be connected to the skeleton.
316
+ secondary_snap_to : None | gpd.GeoSeries = None
317
+ Fall-back series of geometries that shall be connected to the skeleton.
318
+
319
+ Returns
320
+ -------
321
+ to_add, to_split : tuple[list[shapely.LineString], list[shapely.Point]]
322
+ Lines to add and points where to split.
323
+ """
324
+
300
325
to_add = []
301
326
to_split = []
302
- # cast edgelines to gdf
303
- edgelines_df = gpd .GeoDataFrame (geometry = edgelines )
304
- # build queen contiguity on edgelines and extract component labels
305
- comp_labels = graph .Graph .build_contiguity (
306
- edgelines_df [~ (edgelines_df .is_empty | edgelines_df .geometry .isna ())],
307
- rook = False ,
308
- ).component_labels
309
- # compute size of each component
310
- comp_counts = comp_labels .value_counts ()
311
- # get MultiLineString geometry per connected component
312
- components = edgelines_df .dissolve (comp_labels )
327
+
328
+ # generate graph from lines
329
+ comp_labels , comp_counts , components = _prep_components (edgelines )
330
+
331
+ primary_union = shapely .union_all (snap_to )
332
+ secondary_union = shapely .union_all (secondary_snap_to )
313
333
314
334
# if there are muliple components, loop over all and treat each
315
335
if len (components ) > 1 :
316
336
for comp_label , comp in components .geometry .items ():
337
+ cbound = comp .boundary
338
+
317
339
# if component does not interest the boundary, it needs to be snapped
318
340
# if it does but has only one part, this part interesect only on one
319
341
# side (the node remaining from the removed edge) and needs to be
@@ -322,30 +344,25 @@ def snap_to_targets(edgelines, poly, snap_to, secondary_snap_to=None):
322
344
(not comp .intersects (poly .boundary ))
323
345
or comp_counts [comp_label ] == 1
324
346
or (
325
- not comp .intersects (shapely . union_all ( snap_to ) )
347
+ not comp .intersects (primary_union )
326
348
) # ! this fixes one thing but may break others
327
349
):
328
350
# add segment composed of the shortest line to the nearest snapping
329
351
# target. We use boundary to snap to endpoints of edgelines only
330
- sl = shapely .shortest_line (comp . boundary , shapely . union_all ( snap_to ) )
352
+ sl = shapely .shortest_line (cbound , primary_union )
331
353
if _is_within (sl , poly ):
332
- to_add .append (sl )
333
- to_split .append (shapely .get_point (sl , - 1 ))
354
+ to_split , to_add = _split_add (sl , to_split , to_add )
334
355
else :
335
356
if secondary_snap_to is not None :
336
- sl = shapely .shortest_line (
337
- comp .boundary , shapely .union_all (secondary_snap_to )
338
- )
339
- to_split .append (shapely .get_point (sl , - 1 ))
340
- to_add .append (sl )
357
+ sl = shapely .shortest_line (cbound , secondary_union )
358
+ to_split , to_add = _split_add (sl , to_split , to_add )
341
359
else :
342
360
# if there is a single component, ensure it gets a shortest line to an
343
361
# endpoint from each snapping target
344
362
for target in snap_to :
345
363
sl = shapely .shortest_line (components .boundary .item (), target )
346
364
if _is_within (sl , poly ):
347
- to_split .append (shapely .get_point (sl , - 1 ))
348
- to_add .append (sl )
365
+ to_split , to_add = _split_add (sl , to_split , to_add )
349
366
else :
350
367
warnings .warn (
351
368
"Could not create a connection as it would lead outside "
@@ -354,3 +371,31 @@ def snap_to_targets(edgelines, poly, snap_to, secondary_snap_to=None):
354
371
stacklevel = 2 ,
355
372
)
356
373
return to_add , to_split
374
+
375
+
376
+ def _prep_components (lines : np .ndarray ) -> tuple [pd .Series , pd .Series , gpd .GeoSeries ]:
377
+ """Helper for preparing graph components & labels in PySAL."""
378
+
379
+ # cast edgelines to gdf
380
+ lines = gpd .GeoDataFrame (geometry = lines )
381
+
382
+ # build queen contiguity on edgelines and extract component labels
383
+ not_empty = ~ lines .is_empty
384
+ not_nan = ~ lines .geometry .isna ()
385
+ lines = lines [not_empty | not_nan ]
386
+ comp_labels = graph .Graph .build_contiguity (lines , rook = False ).component_labels
387
+
388
+ # compute size of each component
389
+ comp_counts = comp_labels .value_counts ()
390
+
391
+ # get MultiLineString geometry per connected component
392
+ components = lines .dissolve (comp_labels )
393
+
394
+ return comp_labels , comp_counts , components
395
+
396
+
397
+ def _split_add (line : shapely .LineString , splits : list , adds : list ) -> tuple [list ]:
398
+ """Helper for preparing splitter points & added lines."""
399
+ splits .append (shapely .get_point (line , - 1 ))
400
+ adds .append (line )
401
+ return splits , adds
0 commit comments