66
77import geopandas as gpd
88import numpy as np
9+ import pandas as pd
910import shapely
1011from libpysal import graph
1112from scipy import spatial
@@ -122,7 +123,7 @@ def voronoi_skeleton(
122123 buffer : None | float | int = None
123124 Optional custom buffer distance for dealing with Voronoi infinity issues.
124125 secondary_snap_to : None | gpd.GeoSeries = None
125- .. .
126+ Fall-back series of geometries that shall be connected to the skeleton .
126127 limit_distance : None | int = 2
127128 ...
128129 consolidation_tolerance : None | float = None
@@ -236,7 +237,7 @@ def voronoi_skeleton(
236237 if edgelines .shape [0 ] > 0 :
237238 # if there is no explicit snapping target, snap to the boundary of the polygon
238239 # via the shortest line. That is by definition always within the polygon
239- # (Martin thinks)
240+ # (Martin thinks) (James concurs)
240241 if snap_to is not False :
241242 if snap_to is None :
242243 sl = shapely .shortest_line (
@@ -296,24 +297,45 @@ def _consolidate(
296297 return edgelines
297298
298299
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+
300325 to_add = []
301326 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 )
313333
314334 # if there are muliple components, loop over all and treat each
315335 if len (components ) > 1 :
316336 for comp_label , comp in components .geometry .items ():
337+ cbound = comp .boundary
338+
317339 # if component does not interest the boundary, it needs to be snapped
318340 # if it does but has only one part, this part interesect only on one
319341 # 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):
322344 (not comp .intersects (poly .boundary ))
323345 or comp_counts [comp_label ] == 1
324346 or (
325- not comp .intersects (shapely . union_all ( snap_to ) )
347+ not comp .intersects (primary_union )
326348 ) # ! this fixes one thing but may break others
327349 ):
328350 # add segment composed of the shortest line to the nearest snapping
329351 # 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 )
331353 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 )
334355 else :
335356 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 )
341359 else :
342360 # if there is a single component, ensure it gets a shortest line to an
343361 # endpoint from each snapping target
344362 for target in snap_to :
345363 sl = shapely .shortest_line (components .boundary .item (), target )
346364 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 )
349366 else :
350367 warnings .warn (
351368 "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):
354371 stacklevel = 2 ,
355372 )
356373 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