|
| 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 | + |
0 commit comments