Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 26 additions & 15 deletions examples/exportalldata_onecity.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@

Parameters
----------
nominatimstring : str, optional, default Bath
nominatimstring : str, optional, default Barcelona
The string to search for in nominatim/OSM.
export_file_format : str, optional, default "geojson"
File format for the data export. Default "geojson", also possible "gpkg". If exporting as geojson, generates extra files for seed points and city boundary. If exporting as gkpg, these are added all in one file as extra layers.

Notes
-------
Expand All @@ -18,25 +20,34 @@

Examples
--------
>>> python exportalldata_onecity.py Barcelona
>>> python exportalldata_onecity.py Barcelona gpkg
"""

import growbikenet as gbn
import sys

nominatimstring = "Bath"
nominatimstring = "Barcelona"
export_file_format = "geojson"
if len(sys.argv) >= 2:
nominatimstring = sys.argv[1]
print("Exporting data for " + nominatimstring)
if len(sys.argv) >= 3:
export_file_format = sys.argv[2]

print("Exporting " + export_file_format + " data for " + nominatimstring)

for s in ["grid", "rail"]:
print("\n" + "Exporting " + s)
gbn.growbikenet(
city_name=nominatimstring,
proj_crs="3857",
ranking="all",
seed_point_type=s,
export_data=True,
export_plots=False,
export_video=False,
)
for seed_point_type in ["grid", "rail"]:
for ranking in ["betweenness_centrality", "closeness_centrality", "random"]:
for ens in [None, 500]:
if ens: ens_string = ", with existing bike network"
else: ens_string = ""
print("\n" + "Exporting " + seed_point_type + ", " + ranking + ens_string)
gbn.growbikenet(
city_name=nominatimstring,
ranking=ranking,
seed_point_type=seed_point_type,
export_data=True,
export_plots=False,
export_video=False,
export_file_format=export_file_format,
existing_network_spacing=ens,
)
6 changes: 3 additions & 3 deletions examples/mwe.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
import growbikenet as gbn

a_edges = gbn.growbikenet(
city_name="Bath",
proj_crs="3857",
city_name="Oelde",
ranking="betweenness_centrality",
existing_network_spacing=600,
existing_network_spacing=None,
export_file_format="gpkg",
export_data=True,
export_plots=False,
export_video=False,
allow_edge_overlaps=False,
)

# data is saved in current working directory
63 changes: 40 additions & 23 deletions growbikenet/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import osmnx as ox
from scipy.spatial import Delaunay
from shapely.prepared import prep
from shapely.geometry import Point
from shapely.geometry import Point, MultiLineString


def intersects_properly(geom1, geom2):
Expand All @@ -28,7 +28,7 @@ def intersects_properly(geom1, geom2):
return geom1.intersects(geom2) and not geom1.touches(geom2)


def prepare_network(city_name, proj_crs, network_type='all', custom_filter=None, retain_all=True):
def prepare_network(city_name, proj_crs, network_type='all_public', custom_filter=None, retain_all=True):
"""Download and prepare a street network from OSM via OSMnx
Downloads a network with a given network_type and custom_filter using ox.graph_from_place.
Then, stores the undirected OSM data in gdfs and projects using proj_crs.
Expand Down Expand Up @@ -550,6 +550,30 @@ def create_delaunay_edges(nodes_gdf):
return df


def remove_edge_overlaps(edges_in):
"""In the grown network, remove edge overlaps stepwise

Parameters
----------
edges_in: geopandas.geodataframe.GeoDataFrame
The grown bike network, in a projected coordinate reference system

Returns
-------
edges_out: geopandas.geodataframe.GeoDataFrame
The grown bike network without edge overlaps, in a projected coordinate reference system
"""
edges_out = edges_in.copy()
grown_net = MultiLineString()
for row in edges_in.itertuples():
grown_net_new = grown_net | row.geometry # Union
if grown_net_new.length > grown_net.length:
edges_out.loc[row.Index, ['geometry']] = grown_net_new - grown_net # Difference
else: # There was nothing added, so we delete the row
edges_out.drop(index=row.Index, inplace=True)
grown_net = grown_net_new
return edges_out

def df_from_graph(A, method):
"""Create a dataframe from an input graph

Expand All @@ -558,37 +582,31 @@ def df_from_graph(A, method):
A: networkx.graph
Graph created from triangulation edge list
method: str
Method used to rank edges. Must be 'betweenness_centrality' (default), 'closeness_centrality', or 'all'. If 'all', will also add a random ranking.
Method used to rank edges. Must be 'betweenness_centrality' (default), 'closeness_centrality', or 'random'.

Returns
-------
df: pandas.DataFrame
Dataframe with source and target information for each edge, as well as edge attributes as columns
"""

if method == "all":
if method != "random":
attrs = {
edge: {
"betweenness_centrality": data.get("betweenness_centrality"),
"closeness_centrality": data.get("closeness_centrality"),
method: data.get(method),
"geometry": data.get("geometry"),
}
for edge, data in A.edges.items()
}
df = pd.DataFrame.from_dict(
attrs,
orient="index",
columns=["betweenness_centrality", "closeness_centrality", "geometry"],
)
df = pd.DataFrame.from_dict(attrs, orient="index", columns=[method, "geometry"])
else:
attrs = {
edge: {
method: data.get(method),
"geometry": data.get("geometry"),
}
for edge, data in A.edges.items()
}
df = pd.DataFrame.from_dict(attrs, orient="index", columns=[method, "geometry"])
df = pd.DataFrame.from_dict(attrs, orient="index", columns=["geometry"])
df["node_tuple"] = df.index
df["source"] = [t[0] for t in df.node_tuple]
df["target"] = [t[1] for t in df.node_tuple]
Expand All @@ -604,27 +622,26 @@ def rank_df(df, method):
df: pandas.DataFrame
Dataframe with source and target information for each edge, as well as edge attributes as columns
method: str
Method used to rank edges. Must be 'betweenness_centrality' (default), 'closeness_centrality', or 'all'. If 'all', will also add a random ranking.
Method used to rank edges. Must be 'betweenness_centrality' (default), 'closeness_centrality', or 'random'.

Results
-------
df: pandas.DataFrame
Dataframe sorted by specified ranking method.
"""
if method == "all":
for m in ["betweenness_centrality", "closeness_centrality"]:
df = df.sort_values(by=m, ascending=False)
df.reset_index(drop=True, inplace=True)
df["ordering_" + m] = (
df.index
) # ranking is the order of appearance in the method's ranking
if method == "random": # ranking is random
df["ordering_random"] = np.random.permutation(np.arange(df.shape[0]))
else:
df = df.sort_values(by="ordering_random", ascending=False)
df.reset_index(drop=True, inplace=True)
df["ordering_" + method] = (
df.index
)
else: # ranking is the order of appearance in the method's ranking
df = df.sort_values(by=method, ascending=False)
df.reset_index(drop=True, inplace=True)
df["ordering_" + method] = (
df.index
) # ranking is the order of appearance in the method's ranking
)
return df


Expand Down
87 changes: 57 additions & 30 deletions growbikenet/growbikenet.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
prepare_network,
update_with_existing_bike_network,
update_seed_points_with_existing_bike_network,
remove_edge_overlaps,
)
from growbikenet.visualizations import make_video, create_plots

Expand All @@ -36,6 +37,7 @@ def growbikenet(
export_file_format="geojson",
export_plots=False,
export_video=False,
allow_edge_overlaps=False,
):
"""Creates a list of edges ordered by a specified ranking method.

Expand All @@ -48,7 +50,7 @@ def growbikenet(
proj_crs : str, default '3857'
Coordinate reference system that is used to project osm data. Default is '3857' (WGS 84 / Pseudo-Mercator)
ranking : str, default 'betweenness_centrality'
Method used to rank edges. Must be 'betweenness_centrality' (default), 'closeness_centrality', or 'all'. If 'all', will also add a random ranking.
Method used to rank edges. Must be 'betweenness_centrality' (default), 'closeness_centrality', or 'random'.
seed_point_type : str, optional, default 'grid'
If set to 'grid', creates a square grid
If set to 'rail', uses rail stations
Expand All @@ -68,6 +70,8 @@ def growbikenet(
If set to True, plots will be saved to a file
export_video : bool, optional, default False
If set to True, video will be saved to a file (only possible if export_plots is set to True)
allow_edge_overlaps : bool, default False
If set to False, removes edge overlaps in consecutive growth stages. In this case, growth stages that do not add anything new are deleted.

Returns
-------
Expand All @@ -87,9 +91,9 @@ def growbikenet(
raise TypeError("proj_crs must be a string")
if type(ranking) is not str:
raise TypeError("ranking must be a string")
if ranking not in ["betweenness_centrality", "closeness_centrality", "all"]:
if ranking not in ["betweenness_centrality", "closeness_centrality", "random"]:
raise ValueError(
"ranking must be either 'betweenness_centrality', 'closeness_centrality', or 'all'"
"ranking must be either 'betweenness_centrality', 'closeness_centrality', or 'random'"
)
if seed_point_type != "grid" and seed_point_type != "rail":
raise ValueError("seed_point_type must be 'grid' or 'rail'")
Expand Down Expand Up @@ -135,7 +139,7 @@ def growbikenet(
# Due to retain_all=False, this fetches the largest connected component
nodes, edges, g_undir = prepare_network(city_name, proj_crs, network_type='all_public', retain_all=False)

if existing_network_spacing:
if existing_network_spacing: # TO DO: Check for empty bike infra!
nodes, edges, g_undir, nodes_exnw, edges_exnw = update_with_existing_bike_network(city_name, proj_crs, g_undir)

### Create seed points
Expand Down Expand Up @@ -164,9 +168,9 @@ def growbikenet(
if existing_network_spacing:
seed_points_snapped = update_seed_points_with_existing_bike_network(seed_points_snapped, nodes_exnw, existing_network_spacing, proj_crs)

# Abort if only 0 or 1 seed points
if len(seed_points_snapped) < 2:
raise RuntimeError("Found less than 2 seed points, but more are needed.")
# Abort if less than 3 seed points. Delaunay needs at least 3.
if len(seed_points_snapped) < 3:
raise RuntimeError("Found less than 3 seed points, but more are needed.")

### Triangulate
# Triangulation is calculated for the abstract network, but metrics (betweenness, closeness) are calculated for the routed network accounting for lengths.
Expand Down Expand Up @@ -199,13 +203,13 @@ def growbikenet(

### Compute edge attributes
print("Computing edge attributes..")
if ranking == "betweenness_centrality" or ranking == "all":
if ranking == "betweenness_centrality":
# Add betweenness attributes to edges
bc_values = nx.edge_betweenness_centrality(
A, weight="distance", normalized=True
)
nx.set_edge_attributes(A, bc_values, name="betweenness_centrality")
if ranking == "closeness_centrality" or ranking == "all":
elif ranking == "closeness_centrality":
# Add closeness attributes to nodes and edges
cc_values_nodes = nx.closeness_centrality(A, distance="distance")
nx.set_node_attributes(A, cc_values_nodes, name="closeness_centrality")
Expand All @@ -222,15 +226,40 @@ def growbikenet(

a_edges = gpd.GeoDataFrame(a_edges, crs=proj_crs, geometry="geometry")

# Add existing bike network on top, https://stackoverflow.com/a/43408736
if existing_network_spacing:
bikenet = gpd.GeoDataFrame({c: None for c in a_edges.columns}, index=[-1], crs=proj_crs)
bikenet.loc[-1, 'geometry'] = gpd.GeoSeries(edges_exnw.geometry).union_all()
a_edges.loc[-1] = bikenet.loc[-1]
a_edges.index = a_edges.index+1
a_edges.sort_index(inplace=True)
a_edges.crs = proj_crs

# Remove edge overlaps
if not allow_edge_overlaps:
a_edges = remove_edge_overlaps(a_edges)
overlap_string = ""
else:
overlap_string = "_overlap"

# Add lengths and cumulative lengths, rounded to integer meters
a_edges['length'] = a_edges.geometry.length
a_edges['length_cumulative'] = a_edges.geometry.length.cumsum()
a_edges = a_edges.astype({'length': int, 'length_cumulative': int})

# Generate export data filename
if export_data or export_plots or export_video:
os.makedirs("./results/", exist_ok=True)
if export_data_slug is None:
city_string = city_name
else:
city_string = export_data_slug
if existing_network_spacing:
exnw_string = "_with-bikenw"
else:
exnw_string = ""
export_data_filename = (
slugify(city_string) + "-" + ranking + "-" + seed_point_type + "." + export_file_format
slugify(city_string) + "-" + ranking + "-" + seed_point_type + overlap_string + exnw_string + "." + export_file_format
)

# Save to file
Expand All @@ -250,7 +279,11 @@ def growbikenet(
seed_points_snapped.to_file("./results/"+slugify(city_string)+"-"+seed_point_type+".geojson", driver="GeoJSON")
city_boundary.to_file("./results/"+slugify(city_string)+"-city_boundary.geojson", driver="GeoJSON")
elif export_file_format == "gpkg":
a_edges.to_file("./results/"+export_data_filename, driver="GPKG", layer="Bike network")
if existing_network_spacing:
a_edges.iloc[[0]].to_file("./results/"+export_data_filename, driver="GPKG", layer="Existing bike network")
a_edges.iloc[1:-1].to_file("./results/"+export_data_filename, driver="GPKG", layer="Grown bike network", append=True)
else:
a_edges.to_file("./results/"+export_data_filename, driver="GPKG", layer="Grown bike network")
seed_points_snapped.to_file("./results/"+export_data_filename, driver="GPKG", layer="Seed points", append=True)
city_boundary.to_file("./results/"+export_data_filename, driver="GPKG", layer="City boundary", append=True)

Expand All @@ -269,26 +302,20 @@ def growbikenet(
# Define linewidths
lws = {"street": 0.75, "bike": 2}

if ranking == "all":
ranking_list = ["betweenness_centrality", "closeness_centrality", "random"]
else:
ranking_list = [ranking]
for ranking_this in ranking_list:
os.makedirs("./results/plots/ordering_"+ranking_this+"/", exist_ok=True)
create_plots(
routed_edges_gdf,
seed_points_snapped,
streetcolor,
edgecolor,
seedcolor,
lws,
ranking_this,
)
os.makedirs("./results/plots/ordering_"+ranking+"/", exist_ok=True)
create_plots(
routed_edges_gdf,
seed_points_snapped,
streetcolor,
edgecolor,
seedcolor,
lws,
ranking,
)

if export_video:
for ranking_this in ranking_list:
print("Generating video..")
os.makedirs("./results/plots/ordering_"+ranking_this+"/video/", exist_ok=True)
make_video(img_folder_name="./results/plots/ordering_"+ranking_this+"/", fps=5)
print("Generating video..")
os.makedirs("./results/plots/ordering_"+ranking+"/video/", exist_ok=True)
make_video(img_folder_name="./results/plots/ordering_"+ranking+"/", fps=5)

return a_edges
Loading
Loading