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

ENH: add get_nearest_node #600

Merged
merged 7 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion .github/workflows/test_user_guide.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- name: setup micromamba
uses: mamba-org/setup-micromamba@v1
with:
environment-file: ci/envs/312-latest.yaml
Copy link
Member Author

Choose a reason for hiding this comment

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

I am using some of the geopandas 1.0 api (union_all). GeoPandas will be released sooner than momepy.

environment-file: ci/envs/312-dev.yaml

- name: Install momepy
run: pip install .
Expand Down
220 changes: 59 additions & 161 deletions docs/user_guide/elements/links.ipynb

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions momepy/functional/_elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"enclosed_tessellation",
"verify_tessellation",
"get_nearest_street",
"get_nearest_node",
"generate_blocks",
"buffered_limit",
]
Expand Down Expand Up @@ -368,6 +369,57 @@ def get_nearest_street(
return ids


def get_nearest_node(
buildings: GeoSeries | GeoDataFrame,
nodes: GeoDataFrame,
edges: GeoDataFrame,
nearest_edge: Series,
) -> Series:
"""Identify the nearest node for each building.

Snap each building to the closest street network node on the closest network edge.
This assumes that the nearest street network edge has already been identified using
:func:`get_nearest_street`.

The ``edges`` and ``nodes`` GeoDataFrames are expected to be an outcome of
:func:`momepy.nx_to_gdf` or match its structure with ``["node_start", "node_end"]``
columns and their meaning.


Parameters
----------
buildings : GeoSeries | GeoDataFrame
GeoSeries or GeoDataFrame of buildings
martinfleis marked this conversation as resolved.
Show resolved Hide resolved
nodes : GeoDataFrame
A GeoDataFrame containing street nodes.
edges : GeoDataFrame
A GeoDataFrame containing street edges with ``["node_start", "node_end"]``
columns marking start and end nodes of each edge. These are the default
outcome of :func:`momepy.nx_to_gdf`.
nearest_edge : Series
A Series aligned with ``buildings`` containing the information on the nearest
street edge. Matches the outcome of :func:`get_nearest_street`.

Returns
-------
Series
"""
# treat possibly missing edge index
a = np.empty(len(buildings))
na_mask = np.isnan(nearest_edge)
a[na_mask] = np.nan

streets = edges.loc[nearest_edge[~na_mask]]
starts = nodes.loc[streets["node_start"]].distance(buildings[~na_mask], align=False)
ends = nodes.loc[streets["node_end"]].distance(buildings[~na_mask], align=False)
mask = starts.values > ends.values
r = starts.index.to_numpy()
r[mask] = ends.index[mask]

a[~na_mask] = r
return pd.Series(a, index=buildings.index)


def generate_blocks(
tessellation: GeoDataFrame, edges: GeoDataFrame, buildings: GeoDataFrame
) -> tuple[Series, Series, Series]:
Expand Down
47 changes: 47 additions & 0 deletions momepy/functional/tests/test_elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,53 @@ def test_get_nearest_street(self):
nearest = mm.get_nearest_street(self.df_buildings, streets, 10)
assert (nearest == None).sum() == 137 # noqa: E711

def test_get_nearest_node(self):
nodes, edges = mm.nx_to_gdf(mm.gdf_to_nx(self.df_streets))
edge_index = mm.get_nearest_street(self.df_buildings, edges)

node_index = mm.get_nearest_node(self.df_buildings, nodes, edges, edge_index)

assert len(node_index) == len(self.df_buildings)
assert_index_equal(node_index.index, self.df_buildings.index)
expected = np.array(
[
0.0,
1.0,
2.0,
3.0,
4.0,
6.0,
9.0,
11.0,
14.0,
15.0,
16.0,
20.0,
22.0,
25.0,
]
)
expected_counts = np.array([9, 31, 12, 10, 11, 2, 23, 8, 2, 8, 3, 6, 12, 7])
unique, counts = np.unique(node_index, return_counts=True)
np.testing.assert_array_equal(unique, expected)
np.testing.assert_array_equal(counts, expected_counts)

def test_get_nearest_node_missing(self):
nodes, edges = mm.nx_to_gdf(mm.gdf_to_nx(self.df_streets))
edge_index = mm.get_nearest_street(self.df_buildings, edges, max_distance=20)

node_index = mm.get_nearest_node(self.df_buildings, nodes, edges, edge_index)

assert len(node_index) == len(self.df_buildings)
assert_index_equal(node_index.index, self.df_buildings.index)
expected = np.array(
[1.0, 2.0, 3.0, 4.0, 9.0, 11.0, 14.0, 15.0, 16.0, 20.0, 22.0, 25.0, np.nan]
)
expected_counts = np.array([14, 8, 10, 4, 14, 8, 2, 7, 2, 5, 9, 4, 57])
unique, counts = np.unique(node_index, return_counts=True)
np.testing.assert_array_equal(unique, expected)
np.testing.assert_array_equal(counts, expected_counts)

def test_buffered_limit(self):
limit = mm.buffered_limit(self.df_buildings, 50)
assert limit.geom_type == "Polygon"
Expand Down
Loading