Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gdf2osmnx #545

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion momepy/coins.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@


class COINS:

"""
Calculates natural continuity and hierarchy of street networks in a given
GeoDataFrame using the COINS algorithm.
Expand Down
1 change: 0 additions & 1 deletion momepy/distribution.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,6 @@ def __init__(


class Alignment:

"""
Calculate the mean deviation of solar orientation of objects on adjacent cells
from an object.
Expand Down
10 changes: 6 additions & 4 deletions momepy/preprocessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,9 @@ def preprocess(
geoms.append(blg[blg["mm_uid"] == j].iloc[0].geometry)
blg.drop(blg[blg["mm_uid"] == j].index[0], inplace=True)
new_geom = shapely.ops.unary_union(geoms)
blg.loc[
blg.loc[blg["mm_uid"] == key].index[0], blg.geometry.name
] = new_geom
blg.loc[blg.loc[blg["mm_uid"] == key].index[0], blg.geometry.name] = (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this extra indexing needed due to an update in Pandas/GeoPandas?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haven't touched that one (except for running black on it) i'll leave the question to @martinfleis

new_geom
)

blg.drop(delete, inplace=True)
return blg[buildings.columns]
Expand Down Expand Up @@ -1568,7 +1568,9 @@ def __init__(
[gdf.unary_union]
).geoms, # get parts of the collection from polygonize
crs=gdf.crs,
).explode(ignore_index=True) # shouldn't be needed but doesn't hurt to ensure
).explode(
ignore_index=True
) # shouldn't be needed but doesn't hurt to ensure

# Store geometries as a GeoDataFrame
self.polygons = gpd.GeoDataFrame(geometry=polygons)
Expand Down
8 changes: 5 additions & 3 deletions momepy/shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -1258,9 +1258,11 @@ def __init__(self, gdf):
self.gdf = gdf

euclidean = gdf.geometry.apply(
lambda geom: self._dist(geom.coords[0], geom.coords[-1])
if geom.geom_type == "LineString"
else np.nan
lambda geom: (
self._dist(geom.coords[0], geom.coords[-1])
if geom.geom_type == "LineString"
else np.nan
)
)
self.series = euclidean / gdf.geometry.length

Expand Down
1 change: 1 addition & 0 deletions momepy/tests/test_preprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ def test_consolidate_intersections_unsupported(self):
rebuild_edges_method="banana",
)


def test_FaceArtifacts():
pytest.importorskip("esda")
osmnx = pytest.importorskip("osmnx")
Expand Down
11 changes: 11 additions & 0 deletions momepy/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ def test_gdf_to_nx(self):
ValueError, match="Approach 'nonexistent' is not supported."
):
mm.gdf_to_nx(self.df_streets, approach="nonexistent")
with pytest.raises(
ValueError,
match="OSMnx-compatible graphs must be directed, of multigraph type, and using the primal approach.",
):
mm.gdf_to_nx(self.df_streets, approach="dual", osmnx_like=True)

nx = mm.gdf_to_nx(self.df_streets, multigraph=False)
assert isinstance(nx, networkx.Graph)
Expand All @@ -70,6 +75,12 @@ def test_gdf_to_nx(self):
assert nx.number_of_nodes() == 29
assert nx.number_of_edges() == 35

nx = mm.gdf_to_nx(self.df_streets, directed=True, osmnx_like=True)
assert isinstance(nx, networkx.MultiDiGraph)
assert nx.number_of_nodes() == 83
assert nx.number_of_edges() == 89
assert networkx.degree_histogram(nx) == [0, 11, 58, 6, 7, 1]

self.df_streets["oneway"] = True
self.df_streets.loc[0, "oneway"] = False # first road section is bidirectional
nx = mm.gdf_to_nx(self.df_streets, directed=True, oneway_column="oneway")
Expand Down
107 changes: 84 additions & 23 deletions momepy/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
import networkx as nx
import numpy as np
from numpy.lib import NumpyVersion
from shapely.geometry import Point
from shapely.geometry import Point, LineString
from shapely.ops import linemerge

__all__ = [
"unique_id",
Expand Down Expand Up @@ -47,7 +48,14 @@
return abs((a2 - a1 + 180) % 360 - 180)


def _generate_primal(graph, gdf_network, fields, multigraph, oneway_column=None):
def _make_edgetuples(nodelist):
"""Zip list of nodes into list of edgetuples. Helper function for _generate_primal"""
return [z for z in zip(nodelist, nodelist[1:])]


def _generate_primal(
graph, gdf_network, fields, multigraph, osmnx_like, oneway_column=None
):
"""Generate a primal graph. Helper for ``gdf_to_nx``."""
graph.graph["approach"] = "primal"

Expand All @@ -71,24 +79,67 @@
stacklevel=3,
)

key = 0
for row in gdf_network.itertuples():
first = row.geometry.coords[0]
last = row.geometry.coords[-1]
if osmnx_like:

data = list(row)[1:]
attributes = dict(zip(fields, data, strict=True))
if multigraph:
graph.add_edge(first, last, key=key, **attributes)
key += 1

if oneway_column:
oneway = bool(getattr(row, oneway_column))
if not oneway:
graph.add_edge(last, first, key=key, **attributes)
key += 1
else:
graph.add_edge(first, last, **attributes)
gdf_network["geometry"] = gdf_network["geometry"].apply(
lambda x: linemerge(x) if x.geom_type == "MultiLineString" else x
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you doing this? If it is a pre-processing step, than a user should do it themselves. Otherwise this does not round-trip as it should.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_generate_primal already checks for some potential non-linestring geometries, so i thought i'd add this one as well - but yes it is a pre-processing step indeed. fine to leave it out of the function.

)
gdf_network = gdf_network.explode(index_parts=False)
gdf_network = gdf_network[fields]

coords = gdf_network.get_coordinates()
tuples = list(set([(row.x, row.y) for row in coords.itertuples()]))

tupledict = {}
for i, t in enumerate(tuples):
tupledict[t] = i
nodedict = {}
for i, t in enumerate(tuples):
nodedict[i] = t

for (x, y), id in tupledict.items():
graph.add_node(id, x=x, y=y)

edgetuples = gdf_network.apply(
lambda x: _make_edgetuples([tupledict[c] for c in x.geometry.coords]),
axis=1,
)
for row in gdf_network.itertuples():
data = list(row)[1:]
attributes = dict(zip(fields, data, strict=True))
for u, v in edgetuples[row[0]]:
attributes["geometry"] = LineString([nodedict[u], nodedict[v]])
edgedata = graph.get_edge_data(u=u, v=v)
if edgedata:
key = max(edgedata.keys()) + 1

Check warning on line 114 in momepy/utils.py

View check run for this annotation

Codecov / codecov/patch

momepy/utils.py#L114

Added line #L114 was not covered by tests
else:
key = 0
graph.add_edge(u_for_edge=u, v_for_edge=v, key=key, **attributes)
if oneway_column:
oneway = bool(getattr(row, oneway_column))
if not oneway:
graph.add_edge(

Check warning on line 121 in momepy/utils.py

View check run for this annotation

Codecov / codecov/patch

momepy/utils.py#L119-L121

Added lines #L119 - L121 were not covered by tests
u_for_edge=v, v_for_edge=u, key=key + 1, **attributes
)
else:
key = 0
for row in gdf_network.itertuples():
first = row.geometry.coords[0]
last = row.geometry.coords[-1]

data = list(row)[1:]
attributes = dict(zip(fields, data, strict=True))
if multigraph:
graph.add_edge(first, last, key=key, **attributes)
key += 1

if oneway_column:
oneway = bool(getattr(row, oneway_column))
if not oneway:
graph.add_edge(last, first, key=key, **attributes)
key += 1
else:
graph.add_edge(first, last, **attributes)


def _generate_dual(graph, gdf_network, fields, angles, multigraph, angle):
Expand Down Expand Up @@ -150,6 +201,7 @@
angles=True,
angle="angle",
oneway_column=None,
osmnx_like=False,
):
"""
Convert a LineString GeoDataFrame to a ``networkx.MultiGraph`` or other
Expand Down Expand Up @@ -188,7 +240,10 @@
path traversal by specifying the boolean column in the GeoDataFrame. Note,
that the reverse conversion ``nx_to_gdf(gdf_to_nx(gdf, directed=True,
oneway_column="oneway"))`` will contain additional duplicated geometries.

osmnx_like: bool, default False
if osmnx_like=True, returns a MultiDiGraph object that is compatible
with OSMnx functions (edges have (u,v,key), nodes have (x,y) attributes
where coordinates are stored).
Returns
-------
net : networkx.Graph, networkx.MultiGraph, networkx.DiGraph, networkx.MultiDiGraph
Expand Down Expand Up @@ -235,11 +290,16 @@
<networkx.classes.multigraph.MultiGraph object at 0x7f8cf9150fd0>

"""
if osmnx_like and (not directed or not multigraph or approach == "dual"):
raise ValueError(
"OSMnx-compatible graphs must be directed, of multigraph type, and using the primal approach."
)

gdf_network = gdf_network.copy()
if "key" in gdf_network.columns:
gdf_network.rename(columns={"key": "__key"}, inplace=True)

if multigraph and directed:
if (multigraph and directed) or osmnx_like:
net = nx.MultiDiGraph()
elif multigraph and not directed:
net = nx.MultiGraph()
Expand All @@ -251,14 +311,15 @@
net.graph["crs"] = gdf_network.crs
gdf_network[length] = gdf_network.geometry.length
fields = list(gdf_network.columns)

if approach == "primal":
if oneway_column and not directed:
raise ValueError(
"Bidirectional lines are only supported for directed graphs."
)

_generate_primal(net, gdf_network, fields, multigraph, oneway_column)
_generate_primal(
net, gdf_network, fields, multigraph, osmnx_like, oneway_column
)

elif approach == "dual":
if directed:
Expand Down
Loading