Cleaning the graph#
This notebook demonstrate the usage of the main functions for cleaning and fixing topological errors in the GeoDataFrame representing the nodes and the edges of the street network.
[2]:
import sys
from pathlib import Path
repo_root = Path.cwd().resolve().parents[1]
sys.path.insert(0, str(repo_root))
import cityImage as ci
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
fm.fontManager.addfont("times.ttf")
plt.rcParams["font.family"] = "Times New Roman"
[3]:
import osmnx as ox
import cityImage as ci
import pandas as pd
import geopandas as gpd
%matplotlib inline
Functions from the cityImage API used in this notebook#
This appendix illustrates the individual network-cleaning and topology-repair steps that clean_network combines, so you can see and control each operation separately.
network_from_osm — Download a street network from OpenStreetMap and return it as cityImage
nodesandedgesGeoDataFrames.network_from_file — Load a street network from local files into the cityImage
nodes/edgesschema.clean_network — Clean and simplify the network: remove duplicate nodes/edges, pseudo-nodes, dead-ends and self-loops, and repair topology.
clean_duplicate_edges — Remove edges that duplicate the same pair of from/to nodes.
simplify_graph — Simplify the graph by merging interstitial (degree-2) nodes while preserving edge geometry.
remove_disconnected_islands — Drop disconnected sub-networks, keeping the main connected component.
plot_gdf — Plot a single GeoDataFrame with cityImage styling.
For the full list of functions and their parameters, see the API reference.
[4]:
# initialise path, names, etc.
epsg = 3003
crs = 'EPSG:'+str(epsg)
place = 'Rivoli, Italy'
Choose between the following methods:
OSMplace, providing an OSM place name (e.g. City).polygon, providing a Polygon (coordinates must be in units of latitude-longitude degrees).distance_from_address, providing a precise address and setting thedistanceparameter.distance_from_point, providing point coordinates (in units of latitude-longitude degrees) and setting thedistanceparameter to build the bounding box around the point.
[5]:
download_method = 'distance_from_address'
nodes_graph, edges_graph = ci.network_from_osm(place, download_method=download_method, network_type="walk", crs=crs, distance=2000)
[16]:
fig = ci.plot_gdf(nodes_graph, black_background = False, figsize = (10,10), title = place+': Street Network drive',
geometry_size = 3, color = 'red', alpha = 0.5, base_map_gdf = edges_graph, base_map_color = 'blue',
base_map_alpha = 0.7, base_map_geometry_size = 0.6)
/home/jovyan/cityImage/cityImage/plotting/static.py:56: UserWarning: Glyph 8722 (\N{MINUS SIGN}) missing from font(s) Times New Roman.
self.fig.tight_layout(pad=3.0)
The clean functions handle (through boolean parameters):
Duplicate geometries (nodes, edges).
Pseudo-nodes.
remove_islands: Disconnected islands.dead_endsDead-end street segments.self_loopsSelf-Loops.same_vertexes_edgesEdges with same from-to nodes, but different geometries.fix_topologySplits edges and creates a node where two edges share a vertex that was never turned into a junction. It is primarily useful for poorly formed datasets (OSM-derived networks are usually topologically correct). Edges that merely cross without sharing a vertex — including grade-separated bridges and tunnels — are left untouched.
same_vertexes_edges handles edges with same pair of u-v nodes but different geometries. When True, it derives a center line between the two segments, unless one of the two segments is longer than the other (>10%). In this case, the shorter segment is deleted.
Basic Cleaning (Duplicates and Simplification)#
[7]:
nodes_cleaned, edges_cleaned = ci.clean_network(nodes_graph, edges_graph, dead_ends = False, remove_islands = False,
same_vertexes_edges = False, self_loops = False, fix_topology = False,
preserve_direction = False, nodes_to_keep_regardless = [])
[21]:
fig = ci.plot_gdf(edges_cleaned, scheme = None, black_background = False, figsize = (10,10), title =
place+': Raw vs Cleaned Street Networks-study area', color = 'red', geometry_size = 0.8, alpha = 0.9,
base_map_gdf = edges_graph, base_map_color = 'black', base_map_alpha = 0.8, base_map_geometry_size = 1)
/home/jovyan/cityImage/cityImage/plotting/static.py:56: UserWarning: Glyph 8722 (\N{MINUS SIGN}) missing from font(s) Times New Roman.
self.fig.tight_layout(pad=3.0)
Dead-ends and self-loops#
[9]:
nodes_cleaned, edges_cleaned = ci.clean_network(nodes_graph, edges_graph, dead_ends = True, remove_islands = False,
same_vertexes_edges = False, self_loops = True, fix_topology = False,
preserve_direction = False, nodes_to_keep_regardless = [])
[22]:
fig = ci.plot_gdf(edges_cleaned, scheme = None, black_background = False, figsize = (10,10), title =
place+': Raw vs Cleaned Street Networks study area', color = 'red', geometry_size = 0.8, alpha = 0.9,
base_map_gdf = edges_graph, base_map_color = 'black', base_map_alpha = 0.8, base_map_geometry_size = 1)
/home/jovyan/cityImage/cityImage/plotting/static.py:56: UserWarning: Glyph 8722 (\N{MINUS SIGN}) missing from font(s) Times New Roman.
self.fig.tight_layout(pad=3.0)
Graph Simplification (Removing only pseudo-nodes)#
This step cannot be run directly after downloading data from OSM. In fact, OSM represents a street segment with two links. As a consequence, a dead-end junction is confused for a pseudo-node (1 edge x 2 directions = 2 edges), while actual pseudo-nodes are not identified (2 edges x 2 directions = 4). Therefore, before removing the pseudo-nodes is necessary to remove possible duplicates across the edges.
[11]:
nodes_cleaned, edges_cleaned = ci.clean_duplicate_edges(nodes_graph, edges_graph, preserve_direction=False)
# simplify the graph
nodes_cleaned, edges_cleaned = ci.simplify_graph(nodes_cleaned, edges_cleaned, nodes_to_keep_regardless = [])
[23]:
fig = ci.plot_gdf(nodes_cleaned, black_background = False, figsize = (10,10), title = place+': Street Network - drive',
geometry_size = 3, color = 'red', alpha = 0.5, base_map_gdf = edges_cleaned, base_map_color = 'blue',
base_map_alpha = 0.7, base_map_geometry_size = 1.0)
/home/jovyan/cityImage/cityImage/plotting/static.py:56: UserWarning: Glyph 8722 (\N{MINUS SIGN}) missing from font(s) Times New Roman.
self.fig.tight_layout(pad=3.0)
Fixing topology#
While the clean function performs some checks by default (OSM may present some topological errors at times), when one needs to handle data sources where two edges meet at a shared vertex that was never turned into a node, it is advised to perform a full topological check by setting fix_topology to True. Edges that merely cross without sharing a vertex (e.g. grade-separated bridges/tunnels) are left intact. This step can also be run independently via fix_network_topology.
[31]:
input_path = '../data/York/York_street_network.gpkg'
dict_columns = {"highway": "type", "oneway": "oneway", "lanes" :None,
"maxspeed": "maxspeed", "name": "name"}
epsg_york = 2019
nodes_graph, edges_graph = ci.network_from_file(input_path, f"EPSG:{epsg_york}", dict_columns=dict_columns, other_columns=[])
[32]:
fig = ci.plot_gdf(edges_graph, black_background = False, figsize = (10,10), geometry_size = 1.3, column = 'edgeID',
title = "York, Canada: With Topological Errors (i.e. Overlapping Edges)", alpha = 0.9)
/home/jovyan/cityImage/cityImage/plotting/static.py:56: UserWarning: Glyph 8722 (\N{MINUS SIGN}) missing from font(s) Times New Roman.
self.fig.tight_layout(pad=3.0)
[33]:
nodes_cleaned, edges_cleaned = ci.clean_network(nodes_graph, edges_graph, dead_ends = False, remove_islands = False,
same_vertexes_edges = False, self_loops = False, fix_topology = True)
[36]:
fig = ci.plot_gdf(edges_graph, black_background = False, figsize = (10,10), geometry_size = 1.3, column = 'edgeID',
title = "York, Canada: With Corrected Topology", alpha = 0.9)
/home/jovyan/cityImage/cityImage/plotting/static.py:56: UserWarning: Glyph 8722 (\N{MINUS SIGN}) missing from font(s) Times New Roman.
self.fig.tight_layout(pad=3.0)
Removing disconnected Islands#
It may happen that some disconnected set of edges (islands) are included in the representation of the street network. This usually includes few segments that can be disregarded. In this example, a tunnel connection between two urban areas is removed to force the creation of a disconnected island.
[37]:
place = 'Liverpool, UK'
download_method = 'distance_from_address'
distance = 2000
epsg = 27700
nodes_graph, edges_graph = ci.network_from_osm(place, download_method=download_method, network_type="drive", crs=crs, distance=2000)
[38]:
# basic cleaning
nodes_graph, edges_graph = ci.clean_network(nodes_graph, edges_graph, dead_ends = False, remove_islands = False,
same_vertexes_edges = False, self_loops = False, fix_topology = False,
preserve_direction = False, nodes_to_keep_regardless = [])
[ ]:
# forcing a disconnected Island
edges_graph = edges_graph[edges_graph.tunnel != 1]
nodes_cleaned, edges_cleaned = ci.remove_disconnected_islands(nodes_graph, edges_graph)
[41]:
fig = ci.plot_gdf(edges_cleaned, scheme = None, black_background = False, figsize = (10,10), title =
place+': Disconnected Islands Removal', color = 'red', geometry_size = 0.8, alpha = 0.9,
base_map_gdf = edges_graph, base_map_color = 'black', base_map_alpha = 0.5, base_map_geometry_size = 0.6)
/home/jovyan/cityImage/cityImage/plotting/static.py:56: UserWarning: Glyph 8722 (\N{MINUS SIGN}) missing from font(s) Times New Roman.
self.fig.tight_layout(pad=3.0)