Barriers#

[1]:
!pip install osmnx
Collecting osmnx
  Using cached osmnx-2.1.0-py3-none-any.whl.metadata (4.7 kB)
Collecting geopandas>=1.0.1 (from osmnx)
  Downloading geopandas-1.1.4-py3-none-any.whl.metadata (2.3 kB)
Requirement already satisfied: networkx>=2.5 in /opt/conda/lib/python3.12/site-packages (from osmnx) (3.4.2)
Requirement already satisfied: numpy>=1.24 in /opt/conda/lib/python3.12/site-packages (from osmnx) (2.2.6)
Requirement already satisfied: pandas>=1.5 in /opt/conda/lib/python3.12/site-packages (from osmnx) (2.2.3)
Requirement already satisfied: requests>=2.30 in /opt/conda/lib/python3.12/site-packages (from osmnx) (2.32.3)
Collecting shapely>=2.0 (from osmnx)
  Using cached shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (6.8 kB)
Collecting pyogrio>=0.7.2 (from geopandas>=1.0.1->osmnx)
  Downloading pyogrio-0.13.0-cp311-abi3-manylinux_2_28_x86_64.whl.metadata (5.8 kB)
Requirement already satisfied: packaging in /opt/conda/lib/python3.12/site-packages (from geopandas>=1.0.1->osmnx) (25.0)
Collecting pyproj>=3.5.0 (from geopandas>=1.0.1->osmnx)
  Using cached pyproj-3.7.2-cp312-cp312-manylinux_2_28_x86_64.whl.metadata (31 kB)
Requirement already satisfied: python-dateutil>=2.8.2 in /opt/conda/lib/python3.12/site-packages (from pandas>=1.5->osmnx) (2.9.0.post0)
Requirement already satisfied: pytz>=2020.1 in /opt/conda/lib/python3.12/site-packages (from pandas>=1.5->osmnx) (2025.2)
Requirement already satisfied: tzdata>=2022.7 in /opt/conda/lib/python3.12/site-packages (from pandas>=1.5->osmnx) (2025.2)
Requirement already satisfied: certifi in /opt/conda/lib/python3.12/site-packages (from pyogrio>=0.7.2->geopandas>=1.0.1->osmnx) (2025.4.26)
Requirement already satisfied: six>=1.5 in /opt/conda/lib/python3.12/site-packages (from python-dateutil>=2.8.2->pandas>=1.5->osmnx) (1.17.0)
Requirement already satisfied: charset_normalizer<4,>=2 in /opt/conda/lib/python3.12/site-packages (from requests>=2.30->osmnx) (3.4.2)
Requirement already satisfied: idna<4,>=2.5 in /opt/conda/lib/python3.12/site-packages (from requests>=2.30->osmnx) (3.10)
Requirement already satisfied: urllib3<3,>=1.21.1 in /opt/conda/lib/python3.12/site-packages (from requests>=2.30->osmnx) (2.4.0)
Using cached osmnx-2.1.0-py3-none-any.whl (104 kB)
Downloading geopandas-1.1.4-py3-none-any.whl (343 kB)
Downloading pyogrio-0.13.0-cp311-abi3-manylinux_2_28_x86_64.whl (33.3 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 33.3/33.3 MB 88.9 MB/s eta 0:00:00ta 0:00:01
Using cached pyproj-3.7.2-cp312-cp312-manylinux_2_28_x86_64.whl (9.6 MB)
Using cached shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (3.1 MB)
Installing collected packages: shapely, pyproj, pyogrio, geopandas, osmnx
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 5/5 [osmnx]32m3/5 [geopandas]
Successfully installed geopandas-1.1.4 osmnx-2.1.0 pyogrio-0.13.0 pyproj-3.7.2 shapely-2.1.2
[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 pandas as pd
import geopandas as gpd
from matplotlib.colors import LinearSegmentedColormap

%matplotlib inline

import warnings
warnings.simplefilter(action="ignore")

Functions from the cityImage API used in this notebook#

This notebook identifies the structuring barriers of a city — major roads, railways, water features, coastlines and large parks — and assigns their relationship to the street segments. These barriers operationalise Lynch’s edges. Acquisition and file IO are delegated to OSMnx and GeoPandas; cityImage keeps the schema conversion and the barrier semantics.

  • barriers_from_osm — Download structuring barriers (water, railways, parks, major roads) from OpenStreetMap.

  • plot_gdf — Plot a single GeoDataFrame with cityImage styling.

  • network_from_osm — Download a street network from OpenStreetMap and return it as cityImage nodes and edges GeoDataFrames.

  • clean_network — Clean and simplify the network: remove duplicate nodes/edges, pseudo-nodes, dead-ends and self-loops, and repair topology.

  • along_water — Flag street segments that run along water barriers.

  • along_within_parks — Flag street segments that run along or within parks.

  • barriers_along — Assign to each edge the barriers it runs along.

  • assign_structuring_barriers — Assign the crossing/structuring-barrier relationship to edges.

For the full list of functions and their parameters, see the API reference.

[4]:
# initialise path, names, etc.
city_name = 'Paris'
epsg = 27571
crs = 'EPSG:'+str(epsg)
place = 'Paris, Ile-de-France'

Barriers Identification#

Downloading from OSM

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 = 'OSMplace'
[6]:
barriers = ci.barriers_from_osm(
    place,
    download_method=download_method,
    crs=crs,
    include_primary=True,
    include_secondary=False,
    parks_min_area=100000,
)
barriers.reset_index(inplace=True, drop=True)
barriers["barrierID"] = barriers.index.astype(int)
[7]:
# Barriers are already combined by ci.barriers_from_osm().

Visualisation. The cell below maps the result using the cityImage plotting helpers such as plot_gdf; see the Plotting section of the API reference for all styling options.

[8]:
import matplotlib.pyplot as plt

plt.rcParams["font.family"] = "Times New Roman"
[9]:
barriers.sort_values(by = 'barrier_type', ascending = False, inplace = True)
colors = ['green', 'red', 'gray', 'blue']

cmap = LinearSegmentedColormap.from_list('cmap', colors, N=len(colors))
fig = ci.plot_gdf(gdf = barriers, column = 'barrier_type', black_background = False, title = city_name+': Barriers',
                  legend = True, cmap = cmap)
../_images/notebooks_Barriers_13_0.png

Incorporating Barriers into the Street Network#

This is an optional step that allows modelling the effect of barriers on pedestrian movement, for exmaple, in agent-based modelling, or in route-choice modelling.

Download the street network

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.

Downloading the graph and cleaning it (see the notebook 01-Nodes_Paths_fromOSM for details on the cleaning process)

[ ]:
nodes_graph, edges_graph = ci.network_from_osm(place, download_method=download_method, network_type="walk", crs=crs)
nodes_graph, edges_graph = ci.clean_network(nodes_graph, edges_graph, dead_ends = True, remove_islands = True,
                            self_loops = True, same_vertexes_edges = True)

(or) Load it from local path

[12]:
input_path = '../output/'+city_name
nodes_graph = gpd.read_file(input_path+'_nodes.gpkg')
edges_graph = gpd.read_file(input_path+'_edges.gpkg')

nodes_graph.index, edges_graph.index  = nodes_graph.nodeID, edges_graph.edgeID
nodes_graph.index.name, edges_graph.index.name  = None, None

Visualisation. The cell below maps the result using the cityImage plotting helpers such as plot_gdf; see the Plotting section of the API reference for all styling options.

[13]:
fig = ci.plot_gdf(edges_graph, black_background = False, geometry_size = 1.0, alpha = 1.0,
                 color = 'black', title = city_name+': Street Network', figsize = (10,10))
../_images/notebooks_Barriers_20_0.png

Assigning barriers to street segments#

Type of Barriers:

  • Positive barriers, from a pedestrian perspective: Waterbodies, Parks.

  • Negative barriers, from a pedestrian perspective: Major Roads, Railway Structures.

  • Structuring barriers - Barriers which structure and shape the image of the city: Waterbodies, Major roads, Railways.

[14]:
# clipping barriers to case study area
envelope = edges_graph.unary_union.envelope
barriers_within = barriers[barriers.intersects(edges_graph.unary_union.envelope)]

Street Segments Along and Within Positive Barriers#

[15]:
sindex = edges_graph.sindex
# rivers
edges_graph = ci.along_water(edges_graph, barriers_within)
# parks
edges_graph = ci.along_within_parks(edges_graph, barriers_within)
# altogheter
edges_graph['p_barr'] = edges_graph['a_rivers']+edges_graph['w_parks']
edges_graph['p_barr'] = edges_graph.apply(lambda row: list(set(row['p_barr'])), axis = 1)

Street Segments Along Negative Barriers#

[16]:
tmp = barriers_within[barriers_within['barrier_type'].isin(['railway', 'road'])]
edges_graph['n_barr'] = edges_graph.apply(lambda row: ci.barriers_along(row['edgeID'], edges_graph, tmp, sindex,
                                            offset = 25), axis = 1)

Street Segments Crossing any kind of barrier but parks: Structuring Barriers#

[17]:
edges_graph = ci.assign_structuring_barriers(edges_graph, barriers_within)

Visualisation. The cell below maps the result using the cityImage plotting helpers such as plot_gdf; see the Plotting section of the API reference for all styling options.

[18]:
# positive barriers
edges_graph['p_bool'] = edges_graph.apply(lambda row: len(row['p_barr']) > 0, axis = 1)
tmp = edges_graph[edges_graph.p_bool].copy()

# base map
base_map_dict = {'base_map_gdf': edges_graph, 'base_map_alpha' : 0.3, 'base_map_color' : 'black'}
fig = ci.plot_gdf(tmp, black_background = False, figsize = (8, 8), color = 'red', title = city_name+': Streets across/along parks and rivers',
              legend = False, **base_map_dict)
../_images/notebooks_Barriers_30_0.png
[19]:
# negative barriers
edges_graph['n_bool'] = edges_graph.apply(lambda row: len(row['n_barr']) > 0, axis = 1)
tmp = edges_graph[edges_graph.n_bool].copy()

# base map
fig = ci.plot_gdf(tmp, black_background = False, figsize = (8, 8), color = 'red', title = city_name+': Streets along negative barriers',
              legend = False, **base_map_dict)
../_images/notebooks_Barriers_31_0.png
[22]:
# separating barriers
tmp = edges_graph[edges_graph.sep_barr].copy()
fig = ci.plot_gdf(tmp, black_background = False, figsize = (8, 8),  color = 'red',
            title = city_name+': Streets crossing structuring barriers: water, highways, railways',
             **base_map_dict)
../_images/notebooks_Barriers_32_0.png

Exporting. The results are written to disk as GeoPackage files with GeoPandas’ to_file, ready to be reused in later notebooks or in external GIS software.

[23]:
# saving barriers_gdf
output_path = '../output/'+city_name
barriers.to_file(output_path+"_barriers.gpkg", driver='GPKG')

# converting list fields to string
to_convert = ['a_rivers', 'w_parks','n_barr', 'p_barr']
edges_graph_string = edges_graph.copy()
for column in to_convert:
    edges_graph_string[column] = edges_graph_string[column].astype(str)

edges_graph_string.to_file(output_path+"_edges_ped.gpkg", driver='GPKG')
nodes_graph.to_file(output_path+'_nodes_ped.gpkg', driver='GPKG')