Skip to content

Commit

Permalink
Merge branch 'feature/nodes_from_edges_intersections' of github.com:C…
Browse files Browse the repository at this point in the history
…OSIN3-UOC/snkit into COSIN3-UOC-feature/nodes_from_edges_intersections
  • Loading branch information
tomalrussell committed Aug 10, 2022
2 parents f03b0f4 + 1c3be2c commit 846ddc1
Show file tree
Hide file tree
Showing 3 changed files with 479 additions and 44 deletions.
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[tool.pytest.ini_options]
pythonpath = [
"src",
]
133 changes: 127 additions & 6 deletions src/snkit/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
shape,
mapping,
)
from shapely.ops import split, linemerge
from shapely.ops import split, linemerge, unary_union

from collections import Counter

# optional progress bars
if "SNKIT_PROGRESS" in os.environ and os.environ["SNKIT_PROGRESS"] in ("1", "TRUE"):
Expand Down Expand Up @@ -267,12 +269,48 @@ def split_edges_at_nodes(network, tolerance=1e-9):

# combine dfs
edges = pandas.concat(split_edges, axis=0)
# reset index and drop
edges = edges.reset_index().drop("index", axis=1)
# return new network with split edges

return Network(nodes=network.nodes, edges=edges)


def split_edges_at_intersections(network, tolerance=1e-9):
"""Split network edges where they intersect line geometries"""
split_edges = []
split_points = []
for edge in tqdm(
network.edges.itertuples(index=False), desc="split", total=len(network.edges)
):
# note: the symmetry of intersection is not exploited here.
# (If A intersects B, then B intersects A)
# since edges are not modified within the loop, this has just
# potential performance consequences.

hits_points = edges_intersecting_points(edge.geometry, network.edges, tolerance)

# store the split edges and intersection points
split_points.extend(hits_points)
hits_points = MultiPoint(hits_points)
edges = split_edge_at_points(edge, hits_points, tolerance)
split_edges.append(edges)

# add the (potentially) split edges
edges = pandas.concat(split_edges, axis=0)
edges = edges.reset_index().drop("index", axis=1)

# combine the original nodes with the new intersection nodes
# dropping the duplicates.
# note: there are at least duplicates from above since intersections
# are checked twice
# note: intersection nodes are appended, and if any duplicates, the
# original counterparts are kept.
nodes = GeoDataFrame(geometry=split_points)
nodes = pandas.concat([network.nodes, nodes], axis=0).drop_duplicates()
nodes = nodes.reset_index().drop("index", axis=1)

return Network(nodes=nodes, edges=edges)


def link_nodes_to_edges_within(network, distance, condition=None, tolerance=1e-9):
"""Link nodes to all edges within some distance"""
new_node_geoms = []
Expand Down Expand Up @@ -521,6 +559,43 @@ def edges_within(point, edges, distance):
return d_within(point, edges, distance)


def edges_intersecting_points(line, edges, tolerance=1e-9):
"""Return intersection points of intersecting edges"""
hits = edges_intersecting(line, edges, tolerance)

hits_points = []
for hit in hits.geometry:
# first extract the actual intersections from the hits
# for being new geometrical objects, they are not in the sindex
intersection = line.intersection(hit)
# if the line is not simple, there is a self-crossing point
# (note that it will always interact with itself)
# note that __eq__ is used on purpose instead of equals()
# this is stricter: for geometries constructed in the same way
# it makes sense since the sindex is used here
if line == hit and not line.is_simple:
# there is not built-in way to find self-crossing points
# duplicated points after unary_union are the intersections
intersection = unary_union(line)
segments_coordinates = []
for seg in intersection.geoms:
segments_coordinates.extend(list(seg.coords))
intersection = [
Point(p) for p, c in Counter(segments_coordinates).items() if c > 1
]
intersection = MultiPoint(intersection)

# then extract the intersection points
hits_points = intersection_endpoints(intersection, hits_points)

return hits_points


def edges_intersecting(line, edges, tolerance=1e-9):
"""Find edges intersecting line"""
return intersects(line, edges, tolerance)


def nodes_intersecting(line, nodes, tolerance=1e-9):
"""Find nodes intersecting line"""
return intersects(line, nodes, tolerance)
Expand Down Expand Up @@ -569,12 +644,45 @@ def line_endpoints(line):
return start, end


def intersection_endpoints(geom, output=None):
"""Return the points from an intersection geometry
It extracts the starting and ending points of intersection
geometries recursively and appends them to `output`.
This doesn't handle polygons or collections of polygons.
"""
if output is None:
output = []

geom_type = geom.geom_type
if geom.is_empty:
pass
elif geom_type == "Point":
output.append(geom)
elif geom_type == "LineString":
start = Point(geom.coords[0])
end = Point(geom.coords[-1])
output.append(start)
output.append(end)
# recursively for collections of geometries
# note that there is no shared inheritance relationship
elif (
geom_type == "MultiPoint"
or geom_type == "MultiLineString"
or geom_type == "GeometryCollection"
):
for geom_ in geom.geoms:
output = intersection_endpoints(geom_, output)

return output


def split_edge_at_points(edge, points, tolerance=1e-9):
"""Split edge at point/multipoint"""
try:
segments = split_line(edge.geometry, points, tolerance)
except ValueError:
# if splitting fails, e.g. becuase points is empty GeometryCollection
# if splitting fails, e.g. because points is empty GeometryCollection
segments = [edge.geometry]
edges = GeoDataFrame([edge] * len(segments))
edges.geometry = segments
Expand All @@ -584,6 +692,13 @@ def split_edge_at_points(edge, points, tolerance=1e-9):
def split_line(line, points, tolerance=1e-9):
"""Split line at point or multipoint, within some tolerance"""
to_split = snap_line(line, points, tolerance)
# when the splitter is a self-intersection point, shapely splits in
# two parts only in a semi-arbitrary way, see the related question:
# https://gis.stackexchange.com/questions/435879/python-shapely-split-a-complex-line-at-self-intersections?noredirect=1#comment711214_435879
# checking only that the line is complex might not be enough
# but the difference operation is useless in the worst case
if not to_split.is_simple:
to_split = to_split.difference(points)
return list(split(to_split, points).geoms)


Expand Down Expand Up @@ -675,14 +790,20 @@ def to_networkx(network, directed=False, weight_col=None):
G.add_nodes_from(network.nodes.id.to_list())

# add nodal positions from geom
for node_id, x, y in zip(network.nodes.id, network.nodes.geometry.x, network.nodes.geometry.y):
for node_id, x, y in zip(
network.nodes.id, network.nodes.geometry.x, network.nodes.geometry.y
):
G.nodes[node_id]["pos"] = (x, y)

# get edges from network data
if weight_col is None:
# default to geometry length
edges_as_list = list(
zip(network.edges.from_id, network.edges.to_id, network.edges.geometry.length)
zip(
network.edges.from_id,
network.edges.to_id,
network.edges.geometry.length,
)
)
else:
edges_as_list = list(
Expand Down
Loading

0 comments on commit 846ddc1

Please sign in to comment.