Skip to content

Commit

Permalink
Merge pull request #135 from benchmark-urbanism/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
songololo authored Nov 28, 2024
2 parents b68f1f4 + 244c98a commit bc8e1c9
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 28 deletions.
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,9 @@
"editor.defaultFormatter": "rust-lang.rust-analyzer",
"editor.formatOnSave": true
},
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
}
Binary file modified docs/public/images/graph_clean.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/public/images/graph_raw.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions docs/src/pages/rustalgos/rustalgos.md
Original file line number Diff line number Diff line change
Expand Up @@ -2807,12 +2807,12 @@ datapoints are not located with high spatial precision.



<span class="name">node_lives</span><span class="annotation">: list[bool]</span>
<span class="name">node_xs</span><span class="annotation">: list[float]</span>




<span class="name">node_xs</span><span class="annotation">: list[float]</span>
<span class="name">node_lives</span><span class="annotation">: list[bool]</span>



Expand Down
30 changes: 30 additions & 0 deletions docs/src/pages/tools/graphs.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ side-effects as a function of varied node intensities when computing network cen
<span class="pc">:</span>
<span class="pa"> int = 100</span>
</div>
<div class="param">
<span class="pn">remove_deadend_tunnels</span>
<span class="pc">:</span>
<span class="pa"> bool = True</span>
</div>
<span class="pt">)</span>
</div>
</div>
Expand Down Expand Up @@ -160,6 +165,16 @@ side-effects as a function of varied node intensities when computing network cen
Remove disconnected components with fewer nodes than specified by this parameter. Defaults to 100. Set to 0 to keep all disconnected components.</div>
</div>

<div class="param-set">
<div class="def">
<div class="name">remove_deadend_tunnels</div>
<div class="type">bool</div>
</div>
<div class="desc">

Remove dead-end tunnels. Default of True.</div>
</div>

### Returns
<div class="param-set">
<div class="def">
Expand Down Expand Up @@ -346,6 +361,11 @@ side-effects as a function of varied node intensities when computing network cen
<span class="pc">:</span>
<span class="pa"> int = 100</span>
</div>
<div class="param">
<span class="pn">max_foot_tunnel_length</span>
<span class="pc">:</span>
<span class="pa"> int = 50</span>
</div>
<span class="pt">)</span>
</div>
</div>
Expand Down Expand Up @@ -383,6 +403,16 @@ side-effects as a function of varied node intensities when computing network cen
Maximum self loop length to permit for a given edge.</div>
</div>

<div class="param-set">
<div class="def">
<div class="name">max_foot_tunnel_length</div>
<div class="type">int</div>
</div>
<div class="desc">

Maximum tunnel length to permit for non motorised edges.</div>
</div>

### Returns
<div class="param-set">
<div class="def">
Expand Down
5 changes: 5 additions & 0 deletions docs/src/pages/tools/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,11 @@ layout: ../../layouts/PageLayout.astro



<span class="name">levels</span>




<div class="function">

## gather_edge_info
Expand Down
10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "cityseer"
version = '4.16.12'
version = '4.16.14'
description = "Computational tools for network-based pedestrian-scale urban analysis"
readme = "README.md"
requires-python = ">=3.10, <3.14"
Expand Down Expand Up @@ -38,14 +38,14 @@ classifiers = [
]
dependencies = [
"matplotlib>=3.5.1",
"networkx>=2.8.8",
"networkx>=3.0.0",
"pyproj>=3.3.0",
"requests>=2.27.1",
"scikit-learn>=1.0.2",
"tqdm>=4.63.1",
"shapely>=2.0.2",
"numpy>=1.23.3",
"geopandas>=0.12.2",
"shapely>=2.0.0",
"numpy>=2.0.0",
"geopandas>=1.0.0",
"rasterio>=1.3.9",
"fiona>=1.9.6",
"osmnx>=2.0.0",
Expand Down
92 changes: 79 additions & 13 deletions pysrc/cityseer/tools/graphs.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,9 +233,7 @@ def nx_remove_filler_nodes(nx_multigraph: MultiGraph) -> MultiGraph:


def nx_remove_dangling_nodes(
nx_multigraph: MultiGraph,
despine: int = 15,
remove_disconnected: int = 100,
nx_multigraph: MultiGraph, despine: int = 15, remove_disconnected: int = 100, remove_deadend_tunnels: bool = True
) -> MultiGraph:
"""
Remove disconnected components and optionally removes short dead-end street stubs.
Expand All @@ -250,6 +248,8 @@ def nx_remove_dangling_nodes(
remove_disconnected: int
Remove disconnected components with fewer nodes than specified by this parameter. Defaults to 100. Set to 0 to
keep all disconnected components.
remove_deadend_tunnels: bool
Remove dead-end tunnels. Default of True.
Returns
-------
Expand Down Expand Up @@ -289,7 +289,10 @@ def nx_remove_dangling_nodes(
if nx.degree(g_multi_copy, nd_key) == 1:
# only a single neighbour, so index-in directly and update at key = 0
nb_nd_key: NodeKey = list(nx.neighbors(g_multi_copy, nd_key))[0]
if g_multi_copy[nd_key][nb_nd_key][0]["geom"].length <= despine:
edge_data = g_multi_copy[nd_key][nb_nd_key][0]
if (
remove_deadend_tunnels is True and "is_tunnel" in edge_data and edge_data["is_tunnel"] is True
) or edge_data["geom"].length <= despine:
remove_nodes.append(nd_key)
g_multi_copy.remove_nodes_from(remove_nodes)

Expand All @@ -300,25 +303,31 @@ def nx_remove_dangling_nodes(

def _extract_tags_to_set(
tags_list: list[str] | None = None,
) -> set[str]:
) -> set[str | int]:
"""Converts a `list` of `str` tags to a `set` of small caps `str`."""
tags = set()
if tags_list is not None:
if not isinstance(tags_list, list | set | tuple):
raise ValueError(f"Tags should be provided as a `list` of `str` instead of {type(tags_list)}.")
tags_list = [t.strip().lower() for t in tags_list if t not in ["", " ", None]]
cleaned_tags_list = []
for t in tags_list:
if isinstance(t, str):
if t not in ["", " ", None]:
cleaned_tags_list.append(t.strip().lower())
else:
cleaned_tags_list.append(t)
tags.update(tags_list)
return tags


def _tags_from_edge_key(edge_data: EdgeData, edge_key: str) -> set[str]:
def _tags_from_edge_key(edge_data: EdgeData, edge_key: str) -> set[str | int]:
"""Fetches tags from a given edge key and returns as `set` of `str`."""
if edge_key in edge_data:
return _extract_tags_to_set(edge_data[edge_key])
return set()


def _gather_nb_tags(nx_multigraph: MultiGraph, nd_key: NodeKey, edge_key: str) -> set[str]:
def _gather_nb_tags(nx_multigraph: MultiGraph, nd_key: NodeKey, edge_key: str) -> set[str | int]:
"""Fetches tags from edges neighbouring a node and returns as a `set` of `str`."""
nb_tags = set()
for nb_nd_key in nx_multigraph.neighbors(nd_key):
Expand All @@ -327,14 +336,14 @@ def _gather_nb_tags(nx_multigraph: MultiGraph, nd_key: NodeKey, edge_key: str) -
return nb_tags


def _gather_name_tags(edge_data: EdgeData) -> set[str]:
def _gather_name_tags(edge_data: EdgeData) -> set[str | int]:
"""Fetches `names` and `routes` tags from the provided edge and returns as a `set` of `str`."""
names_tags = _tags_from_edge_key(edge_data, "names")
routes_tags = _tags_from_edge_key(edge_data, "routes")
return names_tags.union(routes_tags)


def _gather_nb_name_tags(nx_multigraph: MultiGraph, nd_key: NodeKey) -> set[str]:
def _gather_nb_name_tags(nx_multigraph: MultiGraph, nd_key: NodeKey) -> set[str | int]:
"""Fetches `names` and `routes` tags from edges neighbouring a node and returns as a `set` of `str`."""
names_tags = _gather_nb_tags(nx_multigraph, nd_key, "names")
routes_tags = _gather_nb_tags(nx_multigraph, nd_key, "routes")
Expand Down Expand Up @@ -515,7 +524,10 @@ def nx_snap_endpoints(nx_multigraph: MultiGraph) -> MultiGraph:


def nx_iron_edges(
nx_multigraph: MultiGraph, simplify_by_angle: int = 100, min_self_loop_length: int = 100
nx_multigraph: MultiGraph,
simplify_by_angle: int = 100,
min_self_loop_length: int = 100,
max_foot_tunnel_length: int = 50,
) -> MultiGraph:
"""
Simplifies edges.
Expand All @@ -529,6 +541,8 @@ def nx_iron_edges(
The maximum angle to permit for a given edge. Angles greater than this will be reduced.
min_self_loop_length: int
Maximum self loop length to permit for a given edge.
max_foot_tunnel_length: int
Maximum tunnel length to permit for non motorised edges.
Returns
-------
Expand All @@ -549,6 +563,26 @@ def nx_iron_edges(
if start_nd_key == end_nd_key and edge_geom.length < min_self_loop_length:
remove_edges.append((start_nd_key, end_nd_key, edge_idx))
continue
# drop long foot tunnels
if (
"is_tunnel" in edge_data
and edge_data["is_tunnel"] is True
and edge_data["geom"].length > max_foot_tunnel_length
):
hwy_tags = _tags_from_edge_key(edge_data, "highways")
if not hwy_tags.intersection(
[
"trunk",
"primary",
"secondary",
"tertiary",
"residential",
"service",
]
):
remove_edges.append((start_nd_key, end_nd_key, edge_idx))
continue
# simplify
line_coords = simplify_line_by_angle(edge_geom.coords, simplify_by_angle)
g_multi_copy[start_nd_key][end_nd_key][edge_idx]["geom"] = geometry.LineString(line_coords)
g_multi_copy.remove_edges_from(remove_edges)
Expand Down Expand Up @@ -885,8 +919,9 @@ def recursive_squash(
y: float,
node_group: set[NodeKey],
processed_nodes: set[NodeKey],
_hwy_tags: set[str],
_name_tags: set[str],
_hwy_tags: set,
_name_tags: set,
_levels_tags: set,
recursive: bool = False,
) -> set[NodeKey]:
# keep track of which nodes have been processed as part of recursion
Expand All @@ -908,6 +943,11 @@ def recursive_squash(
continue
if neighbour_policy == "direct" and j_nd_key not in neighbours:
continue
# levels
if _levels_tags:
_nb_level_tags = _gather_nb_tags(nx_multigraph, j_nd_key, "levels")
if not _levels_tags.intersection(_nb_level_tags):
continue
# hwy tags
if osm_hwy_target_tags:
_nb_hwy_tags = _gather_nb_tags(nx_multigraph, j_nd_key, "highways")
Expand All @@ -931,6 +971,7 @@ def recursive_squash(
processed_nodes,
_hwy_tags,
_name_tags,
_levels_tags,
recursive=crawl,
)
return node_group
Expand All @@ -948,6 +989,8 @@ def recursive_squash(
nb_hwy_tags = _gather_nb_tags(nx_multigraph, nd_key, "highways")
if osm_hwy_target_tags and not hwy_tags.intersection(nb_hwy_tags):
continue
# get levels info for matching against potential nodes
nb_levels_tags = _gather_nb_tags(nx_multigraph, nd_key, "levels")
# get name tags for matching against potential matches
nb_name_tags = _gather_nb_name_tags(nx_multigraph, nd_key)
# recurse
Expand All @@ -959,6 +1002,7 @@ def recursive_squash(
set(), # processed nodes tracked through recursion
hwy_tags,
nb_name_tags,
nb_levels_tags,
crawl,
) # whether to recursively probe neighbours per distance
# update removed nodes
Expand Down Expand Up @@ -1011,6 +1055,7 @@ def nx_snap_gapped_endings(
nb_hwy_tags = _gather_nb_tags(nx_multigraph, nd_key, "highways")
if not hwy_tags.intersection(nb_hwy_tags):
continue
nb_levels_tags = _gather_nb_tags(nx_multigraph, nd_key, "levels")
# get name tags for matching against potential gapped edges
nb_name_tags = _gather_nb_name_tags(nx_multigraph, nd_key)
# get all other nodes within the buffer distance
Expand Down Expand Up @@ -1044,6 +1089,11 @@ def nx_snap_gapped_endings(
edge_hwy_tags = _gather_nb_tags(nx_multigraph, j_nd_key, "highways")
if not hwy_tags.intersection(edge_hwy_tags):
continue
# levels
if nb_levels_tags:
edge_level_tags = _gather_nb_tags(nx_multigraph, j_nd_key, "levels")
if not nb_levels_tags.intersection(edge_level_tags):
continue
# names tags
if osm_matched_tags_only is True:
edge_name_tags = _gather_nb_name_tags(nx_multigraph, j_nd_key)
Expand Down Expand Up @@ -1078,6 +1128,7 @@ def nx_snap_gapped_endings(
names=[],
routes=[],
highways=[],
levels=[],
geom=new_geom,
)

Expand Down Expand Up @@ -1208,6 +1259,11 @@ def recurse_child_keys(
continue
# get name tags for matching against potential gapped edges
nb_name_tags = _gather_nb_name_tags(nx_multigraph, nd_key)
# get levels info for matching against potential gapped edges
nb_levels_tags = _gather_nb_tags(nx_multigraph, nd_key, "levels")
# only split from ground level nodes
if nb_levels_tags and 0 not in nb_levels_tags:
continue
# neighbours for filtering out
neighbours = list(nx.neighbors(nx_multigraph, nd_key))
# get all other edges within the buffer distance
Expand Down Expand Up @@ -1266,6 +1322,15 @@ def recurse_child_keys(
# iter gapped edges
for start_nd_key, end_nd_key, edge_idx, edge_data in distinct_edges:
edge_geom = edge_data["geom"]
# don't split on tunnels
if "is_tunnel" in edge_data and edge_data["is_tunnel"] is True:
continue
# level tags
if nb_levels_tags:
# only split on ground levels
edge_levels_tags = _tags_from_edge_key(edge_data, "levels")
if edge_levels_tags and 0 not in edge_levels_tags:
continue
# hwy tags
if osm_hwy_target_tags:
edge_hwy_tags = _tags_from_edge_key(edge_data, "highways")
Expand Down Expand Up @@ -1431,6 +1496,7 @@ def recurse_child_keys(
names=[],
routes=[],
highways=[],
levels=[],
geom=new_geom,
)
# squashing nodes can result in edge duplicates
Expand Down
Loading

0 comments on commit bc8e1c9

Please sign in to comment.