Skip to content

Commit 1230614

Browse files
committed
lts code base
1 parent 03eef3b commit 1230614

File tree

4 files changed

+781
-0
lines changed

4 files changed

+781
-0
lines changed

LTS_OSM.py

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
# ---
2+
# jupyter:
3+
# jupytext:
4+
# formats: ipynb,md,py:light
5+
# text_representation:
6+
# extension: .py
7+
# format_name: light
8+
# format_version: '1.5'
9+
# jupytext_version: 1.14.0
10+
# kernelspec:
11+
# display_name: Python 3 (ipykernel)
12+
# language: python
13+
# name: python3
14+
# ---
15+
16+
# # Level of Traffic Stress maps with Open Street Map
17+
#
18+
# This notebook calculates Level of Traffic Stress from Open Street Map data. It uses the [osmnx](https://osmnx.readthedocs.io/en/stable/) Python package to download a street network.
19+
#
20+
# ### Setup
21+
#
22+
# - download a JSON file from Open Street Map that roughly corresponds to the target area. This is used only to get a list of tags to download with the `osmnx` package. The command `wget -nv -O victoriaosm.osm --post-file=victoriaosm.query "http://overpass-api.de/api/interpreter"` downloads a file named victoriaosm.osm using the query in the file victoriaosm.query. This procedure will be simplified in the future.
23+
24+
import json
25+
import pandas as pd
26+
import numpy as np
27+
import geopandas as gpd
28+
import osmnx as ox
29+
from matplotlib import pyplot as plt
30+
import networkx as nx
31+
from tqdm import tqdm
32+
from mpl_toolkits.axes_grid1 import make_axes_locatable
33+
import matplotlib
34+
35+
# import lts calculation functions
36+
from lts_functions import (biking_permitted, is_separated_path, is_bike_lane, parking_present,
37+
bike_lane_analysis_no_parking, bike_lane_analysis_with_parking, mixed_traffic)
38+
39+
# ## Extract OSM tags to use in download
40+
41+
# load the data
42+
f = open("victoriaosm.osm") # a larger bounding box for the city
43+
data = json.load(f)
44+
45+
# +
46+
# make a dataframe of tags
47+
dfs = []
48+
49+
for element in data['elements']:
50+
if element['type'] != 'way':
51+
continue
52+
df = pd.DataFrame.from_dict(element['tags'], orient = 'index')
53+
dfs.append(df)
54+
# -
55+
56+
tags_df = pd.concat(dfs).reset_index()
57+
tags_df.columns = ["tag", "tagvalue"]
58+
59+
tag_value_counts = tags_df.value_counts().reset_index() # count all the unique tag and value combinations
60+
tag_counts = tags_df['tag'].value_counts().reset_index() # count all the unique tags
61+
62+
# explore the tags that start with 'cycleway'
63+
tag_counts[tag_counts['index'].str.contains('cycleway')]
64+
65+
way_tags = list(tag_counts['index']) # all unique tags from the OSM Toronto download
66+
67+
# add the above list to the global osmnx settings
68+
ox.settings.useful_tags_way += way_tags
69+
ox.settings.osm_xml_way_tags = way_tags
70+
71+
# ### Download data
72+
73+
# ok this actually looks pretty good. Takes out some of the non-bike options automatically.
74+
# might want to include 'footway', but could edit the filter manually too.
75+
osmfilter = ox.downloader._get_osm_filter("bike")
76+
77+
# keep the footway and construction tags - double the size of the dataset...
78+
osmfilter2 = '["highway"]["area"!~"yes"]["access"!~"private"]["highway"!~"abandoned|bus_guideway|corridor|elevator|escalator|motor|planned|platform|proposed|raceway|steps"]["bicycle"!~"no"]["service"!~"private"]'
79+
80+
city = "Victoria"
81+
province = "British Columbia"
82+
83+
# get bike network
84+
G = ox.graph_from_place(
85+
"%s, %s, Canada" %(city, province),
86+
retain_all=True,
87+
truncate_by_edge=True,
88+
simplify=False,
89+
custom_filter=osmfilter2,
90+
)
91+
92+
# save downloaded graph
93+
filepath = "./data/%s.graphml" %city
94+
ox.save_graphml(G, filepath)
95+
96+
# load graph
97+
filepath = "./data/%s.graphml" %city
98+
G = ox.load_graphml(filepath)
99+
100+
# this is slow for a large area
101+
fig, ax = ox.plot_graph(G, node_size=0, edge_color="w", edge_linewidth=0.2)
102+
103+
# you can convert your graph to node and edge GeoPandas GeoDataFrames
104+
gdf_nodes, gdf_edges = ox.graph_to_gdfs(G)
105+
106+
# ## Analyze LTS
107+
#
108+
# Start with is biking allowed, get edges where biking is not *not* allowed.
109+
110+
print(gdf_edges.shape)
111+
gdf_allowed, gdf_not_allowed = biking_permitted(gdf_edges)
112+
print(gdf_allowed.shape)
113+
print(gdf_not_allowed.shape)
114+
115+
# check for separated path
116+
separated_edges, unseparated_edges = is_separated_path(gdf_allowed)
117+
# assign separated ways lts = 1
118+
separated_edges['lts'] = 1
119+
print(separated_edges.shape)
120+
print(unseparated_edges.shape)
121+
122+
to_analyze, no_lane = is_bike_lane(unseparated_edges)
123+
print(to_analyze.shape)
124+
print(no_lane.shape)
125+
126+
parking_detected, parking_not_detected = parking_present(to_analyze)
127+
print(parking_detected.shape)
128+
print(parking_not_detected.shape)
129+
130+
parking_lts = bike_lane_analysis_with_parking(parking_detected)
131+
132+
no_parking_lts = bike_lane_analysis_no_parking(parking_not_detected)
133+
134+
# Next, go to the last step - mixed traffic
135+
136+
lts_no_lane = mixed_traffic(no_lane)
137+
138+
# final components: lts_no_lane, parking_lts, no_parking_lts, separated_edges
139+
# these should all add up to gdf_allowed
140+
print(gdf_allowed.shape)
141+
lts_no_lane.shape[0] + parking_lts.shape[0] + no_parking_lts.shape[0] + separated_edges.shape[0]
142+
143+
gdf_not_allowed['lts'] = 0
144+
145+
all_lts = pd.concat([separated_edges, parking_lts, no_parking_lts, lts_no_lane, gdf_not_allowed])
146+
147+
# decision rule glossary
148+
# these are from Bike Ottawa's stressmodel code
149+
rule_message_dict = {'p2':'Cycling not permitted due to bicycle=\'no\' tag.',
150+
'p6':'Cycling not permitted due to access=\'no\' tag.',
151+
'p3':'Cycling not permitted due to highway=\'motorway\' tag.',
152+
'p4':'Cycling not permitted due to highway=\'motorway_link\' tag.',
153+
'p7':'Cycling not permitted due to highway=\'proposed\' tag.',
154+
'p5':'Cycling not permitted. When footway="sidewalk" is present, there must be a bicycle="yes" when the highway is "footway" or "path".',
155+
's3':'This way is a separated path because highway=\'cycleway\'.',
156+
's1':'This way is a separated path because highway=\'path\'.',
157+
's2':'This way is a separated path because highway=\'footway\' but it is not a crossing.',
158+
's7':'This way is a separated path because cycleway* is defined as \'track\'.',
159+
's8':'This way is a separated path because cycleway* is defined as \'opposite_track\'.',
160+
'b1':'LTS is 1 because there is parking present, the maxspeed is less than or equal to 40, highway="residential", and there are 2 lanes or less.',
161+
'b2':'Increasing LTS to 3 because there are 3 or more lanes and parking present.',
162+
'b3':'Increasing LTS to 3 because the bike lane width is less than 4.1m and parking present.',
163+
'b4':'Increasing LTS to 2 because the bike lane width is less than 4.25m and parking present.',
164+
'b5':'Increasing LTS to 2 because the bike lane width is less than 4.5m, maxspeed is less than 40 on a residential street and parking present.',
165+
'b6':'Increasing LTS to 2 because the maxspeed is between 41-50 km/h and parking present.',
166+
'b7':'Increasing LTS to 3 because the maxspeed is between 51-54 km/h and parking present.',
167+
'b8':'Increasing LTS to 4 because the maxspeed is over 55 km/h and parking present.',
168+
'b9':'Increasing LTS to 3 because highway is not \'residential\'.',
169+
'c1':'LTS is 1 because there is no parking, maxspeed is less than or equal to 50, highway=\'residential\', and there are 2 lanes or less.',
170+
'c3':'Increasing LTS to 3 because there are 3 or more lanes and no parking.',
171+
'c4':'Increasing LTS to 2 because the bike lane width is less than 1.7 metres and no parking.',
172+
'c5':'Increasing LTS to 3 because the maxspeed is between 51-64 km/h and no parking.',
173+
'c6':'Increasing LTS to 4 because the maxspeed is over 65 km/h and no parking.',
174+
'c7':'Increasing LTS to 3 because highway with bike lane is not \'residential\' and no parking.',
175+
'm17':'Setting LTS to 1 because motor_vehicle=\'no\'.',
176+
'm13':'Setting LTS to 1 because highway=\'pedestrian\'.',
177+
'm14':'Setting LTS to 2 because highway=\'footway\' and footway=\'crossing\'.',
178+
'm2':'Setting LTS to 2 because highway=\'service\' and service=\'alley\'.',
179+
'm15':'Setting LTS to 2 because highway=\'track\'.',
180+
'm3':'Setting LTS to 2 because maxspeed is 50 km/h or less and service is \'parking_aisle\'.',
181+
'm4':'Setting LTS to 2 because maxspeed is 50 km/h or less and service is \'driveway\'.',
182+
'm16':'Setting LTS to 2 because maxspeed is less than 35 km/h and highway=\'service\'.',
183+
'm5':'Setting LTS to 2 because maxspeed is up to 40 km/h, 3 or fewer lanes and highway=\'residential\'.',
184+
'm6':'Setting LTS to 3 because maxspeed is up to 40 km/h and 3 or fewer lanes on non-residential highway.',
185+
'm7':'Setting LTS to 3 because maxspeed is up to 40 km/h and 4 or 5 lanes.',
186+
'm8':'Setting LTS to 4 because maxspeed is up to 40 km/h and the number of lanes is greater than 5.',
187+
'm9':'Setting LTS to 2 because maxspeed is up to 50 km/h and lanes are 2 or less and highway=\'residential\'.',
188+
'm10':'Setting LTS to 3 because maxspeed is up to 50 km/h and lanes are 3 or less on non-residential highway.',
189+
'm11':'Setting LTS to 4 because the number of lanes is greater than 3.',
190+
'm12':'Setting LTS to 4 because maxspeed is greater than 50 km/h.'}
191+
192+
193+
simplified_message_dict = {'p2':r'bicycle $=$ "no"',
194+
'p6':r'access $=$ "no"',
195+
'p3':r'highway $=$ "motorway"',
196+
'p4':r'highway $=$ "motorway_link"',
197+
'p7':r'highway $=$ "proposed"',
198+
'p5':r'footway $=$ "sidewalk", bicycle$\neq$"yes"',
199+
's3':r'highway $=$ "cycleway"',
200+
's1':r'highway $=$" path"',
201+
's2':r'separated, highway $=$" footway", not a crossing',
202+
's7':r'cycleway* $=$ "track"',
203+
's8':r'cycleway* $=$ "opposite_track"',
204+
'b1':r'bike lane w/ parking, $\leq$ 40 km/h, highway $=$ "residential", $\leq$ 2 lanes',
205+
'b2':r'bike lane w/ parking, 3 or more lanes',
206+
'b3':r'bike lane width $<$ 4.1m, parking',
207+
'b4':r'bike lane width $<$ 4.25m, parking',
208+
'b5':r'bike lane width $<$ 4.5m, $\leq$ 40 km/h, residential, parking',
209+
'b6':r'bike lane w/ parking, speed 41-50 km/h',
210+
'b7':r'bike lane w/ parking, speed 51-54 km/h',
211+
'b8':r'bike lane w/ parking, speed $>$ 55 km/h',
212+
'b9':r'bike lane w/ parking, highway $\neq$ "residential"',
213+
'c1':r'bike lane no parking, $\leq$ 50 km/h, highway $=$ "residential", $\leq$ 2 lanes',
214+
'c3':r'bike lane no parking, $\leq$ 65 km/h, $\geq$ 3 lanes',
215+
'c4':r'bike lane width $<$ 1.7m, no parking',
216+
'c5':r'bike lane no parking, speed 51-64 km/h',
217+
'c6':r'bike lane no parking, speed $>$ 65 km/h',
218+
'c7':r'bike lane no parking, highway $\neq$ "residential"',
219+
'm17':r'mixed traffic, motor_vehicle $=$ "no"',
220+
'm13':r'mixed traffic, highway $=$ "pedestrian"',
221+
'm14':r'mixed traffic, highway $=$ "footway", footway $=$ "crossing"',
222+
'm2':r'mixed traffic, highway $=$ "service", service $=$ "alley"',
223+
'm15':r'mixed traffic, highway $=$ "track"',
224+
'm3':r'mixed traffic, speed $\leq$ 50 km/h, service $=$ "parking_aisle"',
225+
'm4':r'mixed traffic, speed $\leq$ 50 km/h, service $=$ "driveway"',
226+
'm16':r'mixed traffic, speed $\leq$ 35 km/h, highway $=$ "service"',
227+
'm5':r'mixed traffic, speed $\leq$ 40 km/h, highway $=$ "residential", $\leq$ 3 lanes',
228+
'm6':r'mixed traffic, speed $\leq$ 40 km/h, highway $\neq$ "residential", $\leq$ 3 lanes',
229+
'm7':r'mixed traffic, speed $\leq$ 40 km/h, 4 or 5 lanes',
230+
'm8':r'mixed traffic, speed $\leq$ 40 km/h, lanes $>$ 5',
231+
'm9':r'mixed traffic, speed $\leq$ 50 km/h, highway $=$ "residential",$\leq$ 2 lanes',
232+
'm10':r'mixed traffic, speed $\leq$ 50 km/h, highway $\neq$ "residential", $\leq$ 3 lanes',
233+
'm11':r'mixed traffic, speed $\leq$ 50 km/h, lanes $>$ 3',
234+
'm12':r'mixed traffic, speed $>$ 50 km/h'}
235+
236+
all_lts['message'] = all_lts['rule'].map(rule_message_dict)
237+
all_lts['short_message'] = all_lts['rule'].map(simplified_message_dict)
238+
239+
# ## Node LTS
240+
#
241+
# Calculate node LTS.
242+
#
243+
# - An intersection without either was assigned the highest LTS of its intersecting roads.
244+
# - Stop signs reduced an otherwise LTS2 intersection to LTS1.
245+
# - A signalized intersection of two lowstress links was assigned LTS1.
246+
# - Assigned LTS2 to signalized intersections where a low-stress (LTS1/ 2) link crosses a high-stress (LTS3/4) link.
247+
248+
gdf_nodes['highway'].value_counts()
249+
250+
# +
251+
gdf_nodes['lts'] = np.nan # make lts column
252+
gdf_nodes['message'] = np.nan # make message column
253+
254+
for node in tqdm(gdf_nodes.index):
255+
try:
256+
edges = all_lts.loc[node]
257+
except:
258+
#print("Node not found in edges: %s" %node)
259+
gdf_nodes.loc[node, 'message'] = "Node not found in edges"
260+
continue
261+
control = gdf_nodes.loc[node,'highway'] # if there is a traffic control
262+
max_lts = edges['lts'].max()
263+
node_lts = int(max_lts) # set to max of intersecting roads
264+
message = "Node LTS is max intersecting LTS"
265+
if node_lts > 2:
266+
if control == 'traffic_signals':
267+
node_lts = 2
268+
message = "LTS 3-4 with traffic signals"
269+
elif node_lts <= 2:
270+
if control == 'traffic_signals' or control == 'stop':
271+
node_lts = 1
272+
message = "LTS 1-2 with traffic signals or stop"
273+
274+
gdf_nodes.loc[node,'message'] = message
275+
gdf_nodes.loc[node,'lts'] = node_lts # assign node lts
276+
# -
277+
278+
# ### Save data for plotting
279+
280+
gdf_nodes.to_csv("data/gdf_nodes_%s.csv" %city)
281+
282+
all_lts_small = all_lts[['osmid', 'lanes', 'name', 'highway', 'maxspeed', 'geometry', 'length', 'rule', 'lts',
283+
'lanes_assumed', 'maxspeed_assumed', 'message', 'short_message']]
284+
all_lts_small.to_csv("data/all_lts_%s.csv" %city)
285+
286+
# make graph with LTS information
287+
G_lts = ox.graph_from_gdfs(gdf_nodes, all_lts_small)
288+
289+
# save LTS graph
290+
filepath = "data/%s_lts.graphml" %city
291+
ox.save_graphml(G_lts, filepath)
292+
293+

LTS_plot.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# ---
2+
# jupyter:
3+
# jupytext:
4+
# formats: ipynb,py:light,md
5+
# text_representation:
6+
# extension: .py
7+
# format_name: light
8+
# format_version: '1.5'
9+
# jupytext_version: 1.14.0
10+
# kernelspec:
11+
# display_name: Python 3 (ipykernel)
12+
# language: python
13+
# name: python3
14+
# ---
15+
16+
# # Plotting Level of Traffic Stress
17+
#
18+
# This notebook plots the Level of Traffic Stress map calculated in `LTS_OSM'.
19+
20+
import numpy as np
21+
import pandas as pd
22+
import geopandas as gpd
23+
from matplotlib import pyplot as plt
24+
25+
city = "Victoria"
26+
27+
28+
all_lts_df = pd.read_csv("data/all_lts_%s.csv" %city)
29+
30+
# convert to a geodataframe for plotting
31+
all_lts = gpd.GeoDataFrame(
32+
all_lts_df.loc[:, [c for c in all_lts_df.columns if c != "geometry"]],
33+
geometry=gpd.GeoSeries.from_wkt(all_lts_df["geometry"]),
34+
crs='wgs84') # projection from graph
35+
36+
gdf_nodes = pd.read_csv("data/gdf_nodes_%s.csv" %city, index_col=0)
37+
38+
# +
39+
# define lts colours for plotting
40+
conditions = [
41+
(all_lts['lts'] == 1),
42+
(all_lts['lts'] == 2),
43+
(all_lts['lts'] == 3),
44+
(all_lts['lts'] == 4),
45+
]
46+
47+
# create a list of the values we want to assign for each condition
48+
values = ['g', 'b', 'y', 'r']
49+
50+
# create a new column and use np.select to assign values to it using our lists as arguments
51+
all_lts['color'] = np.select(conditions, values)
52+
# -
53+
54+
fig, ax = plt.subplots()
55+
all_lts[all_lts['lts'] > 0].plot(ax = ax, linewidth = 0.1, color = all_lts[all_lts['lts'] > 0]['color'])
56+
plt.savefig("lts_%s.pdf" %city)
57+
plt.savefig("lts_%s.png" %city, dpi = 300)
58+
59+
# ## Plot segments that aren't missing speed and lane info
60+
61+
has_speed_lanes = all_lts[(~all_lts['maxspeed'].isna())
62+
& (~all_lts['lanes'].isna())]
63+
64+
# +
65+
fig, ax = plt.subplots(figsize = (8,8))
66+
has_speed_lanes.plot(ax = ax, linewidth = 0.5, color = has_speed_lanes['color'])
67+
68+
ax.set_xlim(-79.43, -79.37)
69+
ax.set_ylim(43.645, 43.675)
70+
71+
ax.set_yticks([])
72+
ax.set_xticks([])
73+
74+
plt.savefig("LTS_%s_has_speed_has_lanes.pdf" %city)
75+
plt.savefig("LTS_%s_has_speed_has_lanes.png" %city, dpi = 300)
76+
# -
77+
78+

0 commit comments

Comments
 (0)