2727 shape ,
2828 mapping ,
2929)
30- from shapely .ops import split , linemerge
30+ from shapely .ops import split , linemerge , unary_union
31+
32+ from collections import Counter
3133
3234# optional progress bars
3335if "SNKIT_PROGRESS" in os .environ and os .environ ["SNKIT_PROGRESS" ] in ("1" , "TRUE" ):
@@ -267,12 +269,48 @@ def split_edges_at_nodes(network, tolerance=1e-9):
267269
268270 # combine dfs
269271 edges = pandas .concat (split_edges , axis = 0 )
270- # reset index and drop
271272 edges = edges .reset_index ().drop ("index" , axis = 1 )
272- # return new network with split edges
273+
273274 return Network (nodes = network .nodes , edges = edges )
274275
275276
277+ def split_edges_at_intersections (network , tolerance = 1e-9 ):
278+ """Split network edges where they intersect line geometries"""
279+ split_edges = []
280+ split_points = []
281+ for edge in tqdm (
282+ network .edges .itertuples (index = False ), desc = "split" , total = len (network .edges )
283+ ):
284+ # note: the symmetry of intersection is not exploited here.
285+ # (If A intersects B, then B intersects A)
286+ # since edges are not modified within the loop, this has just
287+ # potential performance consequences.
288+
289+ hits_points = edges_intersecting_points (edge .geometry , network .edges , tolerance )
290+
291+ # store the split edges and intersection points
292+ split_points .extend (hits_points )
293+ hits_points = MultiPoint (hits_points )
294+ edges = split_edge_at_points (edge , hits_points , tolerance )
295+ split_edges .append (edges )
296+
297+ # add the (potentially) split edges
298+ edges = pandas .concat (split_edges , axis = 0 )
299+ edges = edges .reset_index ().drop ("index" , axis = 1 )
300+
301+ # combine the original nodes with the new intersection nodes
302+ # dropping the duplicates.
303+ # note: there are at least duplicates from above since intersections
304+ # are checked twice
305+ # note: intersection nodes are appended, and if any duplicates, the
306+ # original counterparts are kept.
307+ nodes = GeoDataFrame (geometry = split_points )
308+ nodes = pandas .concat ([network .nodes , nodes ], axis = 0 ).drop_duplicates ()
309+ nodes = nodes .reset_index ().drop ("index" , axis = 1 )
310+
311+ return Network (nodes = nodes , edges = edges )
312+
313+
276314def link_nodes_to_edges_within (network , distance , condition = None , tolerance = 1e-9 ):
277315 """Link nodes to all edges within some distance"""
278316 new_node_geoms = []
@@ -521,6 +559,43 @@ def edges_within(point, edges, distance):
521559 return d_within (point , edges , distance )
522560
523561
562+ def edges_intersecting_points (line , edges , tolerance = 1e-9 ):
563+ """Return intersection points of intersecting edges"""
564+ hits = edges_intersecting (line , edges , tolerance )
565+
566+ hits_points = []
567+ for hit in hits .geometry :
568+ # first extract the actual intersections from the hits
569+ # for being new geometrical objects, they are not in the sindex
570+ intersection = line .intersection (hit )
571+ # if the line is not simple, there is a self-crossing point
572+ # (note that it will always interact with itself)
573+ # note that __eq__ is used on purpose instead of equals()
574+ # this is stricter: for geometries constructed in the same way
575+ # it makes sense since the sindex is used here
576+ if line == hit and not line .is_simple :
577+ # there is not built-in way to find self-crossing points
578+ # duplicated points after unary_union are the intersections
579+ intersection = unary_union (line )
580+ segments_coordinates = []
581+ for seg in intersection .geoms :
582+ segments_coordinates .extend (list (seg .coords ))
583+ intersection = [
584+ Point (p ) for p , c in Counter (segments_coordinates ).items () if c > 1
585+ ]
586+ intersection = MultiPoint (intersection )
587+
588+ # then extract the intersection points
589+ hits_points = intersection_endpoints (intersection , hits_points )
590+
591+ return hits_points
592+
593+
594+ def edges_intersecting (line , edges , tolerance = 1e-9 ):
595+ """Find edges intersecting line"""
596+ return intersects (line , edges , tolerance )
597+
598+
524599def nodes_intersecting (line , nodes , tolerance = 1e-9 ):
525600 """Find nodes intersecting line"""
526601 return intersects (line , nodes , tolerance )
@@ -569,12 +644,45 @@ def line_endpoints(line):
569644 return start , end
570645
571646
647+ def intersection_endpoints (geom , output = None ):
648+ """Return the points from an intersection geometry
649+
650+ It extracts the starting and ending points of intersection
651+ geometries recursively and appends them to `output`.
652+ This doesn't handle polygons or collections of polygons.
653+ """
654+ if output is None :
655+ output = []
656+
657+ geom_type = geom .geom_type
658+ if geom .is_empty :
659+ pass
660+ elif geom_type == "Point" :
661+ output .append (geom )
662+ elif geom_type == "LineString" :
663+ start = Point (geom .coords [0 ])
664+ end = Point (geom .coords [- 1 ])
665+ output .append (start )
666+ output .append (end )
667+ # recursively for collections of geometries
668+ # note that there is no shared inheritance relationship
669+ elif (
670+ geom_type == "MultiPoint"
671+ or geom_type == "MultiLineString"
672+ or geom_type == "GeometryCollection"
673+ ):
674+ for geom_ in geom .geoms :
675+ output = intersection_endpoints (geom_ , output )
676+
677+ return output
678+
679+
572680def split_edge_at_points (edge , points , tolerance = 1e-9 ):
573681 """Split edge at point/multipoint"""
574682 try :
575683 segments = split_line (edge .geometry , points , tolerance )
576684 except ValueError :
577- # if splitting fails, e.g. becuase points is empty GeometryCollection
685+ # if splitting fails, e.g. because points is empty GeometryCollection
578686 segments = [edge .geometry ]
579687 edges = GeoDataFrame ([edge ] * len (segments ))
580688 edges .geometry = segments
@@ -584,6 +692,13 @@ def split_edge_at_points(edge, points, tolerance=1e-9):
584692def split_line (line , points , tolerance = 1e-9 ):
585693 """Split line at point or multipoint, within some tolerance"""
586694 to_split = snap_line (line , points , tolerance )
695+ # when the splitter is a self-intersection point, shapely splits in
696+ # two parts only in a semi-arbitrary way, see the related question:
697+ # https://gis.stackexchange.com/questions/435879/python-shapely-split-a-complex-line-at-self-intersections?noredirect=1#comment711214_435879
698+ # checking only that the line is complex might not be enough
699+ # but the difference operation is useless in the worst case
700+ if not to_split .is_simple :
701+ to_split = to_split .difference (points )
587702 return list (split (to_split , points ).geoms )
588703
589704
@@ -675,14 +790,20 @@ def to_networkx(network, directed=False, weight_col=None):
675790 G .add_nodes_from (network .nodes .id .to_list ())
676791
677792 # add nodal positions from geom
678- for node_id , x , y in zip (network .nodes .id , network .nodes .geometry .x , network .nodes .geometry .y ):
793+ for node_id , x , y in zip (
794+ network .nodes .id , network .nodes .geometry .x , network .nodes .geometry .y
795+ ):
679796 G .nodes [node_id ]["pos" ] = (x , y )
680797
681798 # get edges from network data
682799 if weight_col is None :
683800 # default to geometry length
684801 edges_as_list = list (
685- zip (network .edges .from_id , network .edges .to_id , network .edges .geometry .length )
802+ zip (
803+ network .edges .from_id ,
804+ network .edges .to_id ,
805+ network .edges .geometry .length ,
806+ )
686807 )
687808 else :
688809 edges_as_list = list (
0 commit comments