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 nodes and edges GeoDataFrames.

  • network_from_file — Load a street network from local files into the cityImage nodes/edges schema.

  • 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 the distance parameter.

  • distance_from_point, providing point coordinates (in units of latitude-longitude degrees) and setting the distance parameter 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)
../_images/notebooks_NetworkCleaning_8_1.png

The clean functions handle (through boolean parameters):

  • Duplicate geometries (nodes, edges).

  • Pseudo-nodes.

  • remove_islands: Disconnected islands.

  • dead_ends Dead-end street segments.

  • self_loops Self-Loops.

  • same_vertexes_edges Edges with same from-to nodes, but different geometries.

  • fix_topology Splits 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)
../_images/notebooks_NetworkCleaning_12_1.png

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)
../_images/notebooks_NetworkCleaning_15_1.png

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)
../_images/notebooks_NetworkCleaning_19_1.png

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)
../_images/notebooks_NetworkCleaning_22_1.png
[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)
../_images/notebooks_NetworkCleaning_24_1.png

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)
../_images/notebooks_NetworkCleaning_30_1.png