From 8b225aa0bee34a2bfaf7f4c2d7065203da84d1d6 Mon Sep 17 00:00:00 2001 From: Manuel-Knepper Date: Mon, 10 Aug 2026 09:31:16 +0200 Subject: [PATCH 1/5] export geojson with espg 4326 and RFC7946 set to 'yes' --- linkbikenet/linkbikenet.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/linkbikenet/linkbikenet.py b/linkbikenet/linkbikenet.py index 45c0a18..b9b97b8 100644 --- a/linkbikenet/linkbikenet.py +++ b/linkbikenet/linkbikenet.py @@ -149,6 +149,10 @@ def linkbikenet( edges_pbi_gdf = edges_gdf[edges_gdf["pbi"] == 1] + # Back to unprojected (potentially). No more calculations after here. + gdf.to_crs(epsg=4326, inplace=True) + edges_pbi_gdf.to_crs(epsg=4326, inplace=True) + # Generate export data filename if export_data: os.makedirs("./results/", exist_ok=True) @@ -161,16 +165,11 @@ def linkbikenet( print("Saving data..") edges_pbi_gdf.drop(["osmid"], axis=1, inplace=True) city_boundary = ox.geocoder.geocode_to_gdf(city_name) - city_boundary.to_crs(epsg=proj_crs, inplace=True) - # We have meter precision, so rounding to integers is fine. Better would be to - # change dtypes to int, but this does not seem possible without manual looping. - city_boundary.geometry = city_boundary.geometry.set_precision(grid_size=1) - edges_pbi_gdf.geometry = edges_pbi_gdf.geometry.set_precision(grid_size=1) - gdf.geometry = gdf.geometry.set_precision(grid_size=1) + city_boundary.to_crs(epsg=4326, inplace=True) if export_file_format == "geojson": - gdf.to_file("./results/" + export_data_filename, driver="GeoJSON") - edges_pbi_gdf.to_file("./results/" + city_name + "-existing_bike_network.geojson", driver="GeoJSON") - city_boundary.to_file("./results/" + city_name + "-city_boundary.geojson", driver="GeoJSON") + gdf.to_file("./results/" + export_data_filename, driver="GeoJSON", RFC7946="YES") + edges_pbi_gdf.to_file("./results/" + city_name + "-existing_bike_network.geojson", driver="GeoJSON", RFC7946="YES") + city_boundary.to_file("./results/" + city_name + "-city_boundary.geojson", driver="GeoJSON", RFC7946="YES") elif export_file_format == "gpkg": gdf.to_file("./results/" + export_data_filename, driver="GPKG", layer="Identified links") edges_pbi_gdf.to_file("./results/" + export_data_filename, driver="GPKG", layer="Existing bike network", From 533c42326f8b963fe189f199bf3735b82fb90d34 Mon Sep 17 00:00:00 2001 From: Manuel-Knepper Date: Mon, 10 Aug 2026 09:54:03 +0200 Subject: [PATCH 2/5] updated names for file export, added slugify function and implemented it --- linkbikenet/functions.py | 31 ++++++++++++++++++++++++++++++- linkbikenet/linkbikenet.py | 37 ++++++++++++++++++++++--------------- linkbikenet/settings.py | 1 + 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/linkbikenet/functions.py b/linkbikenet/functions.py index 30415e9..87c4729 100644 --- a/linkbikenet/functions.py +++ b/linkbikenet/functions.py @@ -1,5 +1,6 @@ from . import config from . import settings +import re import osmnx as ox import geopandas as gpd import numpy as np @@ -329,4 +330,32 @@ def create_gdf_with_geoms(df, edges): gdf = gpd.GeoDataFrame(df, crs=edges.crs, geometry="geometry") # merge multilinestring into linestring where possible (should be possible everywhere) gdf["geometry"] = gdf.line_merge() - return gdf \ No newline at end of file + return gdf + + +def slugify(s): + """Slugify a string + + Source: https://github.com/Chalarangelo/30-seconds-of-code/blob/master/content/snippets/python/s/slugify.md + Note: A clean global solution would be using unidecode, but we do not want extra dependencies for this. We assume European city names in latin alphabet, some special letters like Hungarian long ö already mapped. + + Parameters + ---------- + s : str + String to slufigy + + Returns + ------- + s : str + Slugified string + """ + s = s.lower().strip() + s = re.sub(r'[\s-]+', '', s) # Remove white spaces, - + s = re.sub(r'[^\w\s-]', '', s) + s = re.sub(r'^-+|-+$', '', s) + tab = str.maketrans( + "áéíóúàèìùòâêîôûäëïöüǎěǐǒǔãẽĩõũăåæçčıłñňøœřßșşšůŷÿźž", + "aeiouaeiouaeiouaeiouaeiouaeiouaaaccilnnoorssssuyyzz" + ) + s = s.translate(tab) + return s \ No newline at end of file diff --git a/linkbikenet/linkbikenet.py b/linkbikenet/linkbikenet.py index b9b97b8..3bbfa82 100644 --- a/linkbikenet/linkbikenet.py +++ b/linkbikenet/linkbikenet.py @@ -7,10 +7,11 @@ from linkbikenet.functions import * def linkbikenet( - city_name, + city_query, connection_strategy = "largest", proj_crs = "3857", export_data = True, + city_id = None, export_file_format = "geojson", import_files={}, ): @@ -18,14 +19,16 @@ def linkbikenet( Creates links between components of bicycle networks in cities. How components are connected depends on the connection strategy that was chosen. Parameters ---------- - city_name : str - name of the city that the analysis should be performed on + city_query : str + Search string for the city that the analysis should be performed on. This is the query used to fetch the data from nominatim. connection_strategy : str, default="largest strategy to use for connecting between components. Default is "largest", other options are "largest-closest" and "closest" proj_crs : str, default '3857' coordinate reference system that is used to project osm data. Default is '3857' (WGS 84 / Pseudo-Mercator) export_data : bool, optional, default True - If set to True, data will be saved to a file. The filename is [slug].gpkg, where slug is a string id made out of city_name + If set to True, data will be saved to a file. The filename is [slug].gpkg, where slug is a string id made out of city_query + city_id : str | None, default None + If set, the slugified city_id is used in the filename of the data export. For example, a city_id "Athens" will slugify into "athens" in filenames. If set to None, the slugified city_query is used in the filename of the data export. It is useful to set a city_id for cities where the city_query is not the city name, for example to set for a city_query "Municipality of Athens" the city_id to "Athens". export_file_format : str, optional, default "geojson" File format for the data export, relevant if export_data set to True. Default "geojson", also possible "gpkg". If exporting as geojson, generates extra files for street network and city boundary. If exporting as gkpg, these are added all in one file as extra layers. import_files: dict, default {} @@ -42,7 +45,7 @@ def linkbikenet( geodataframe with the proposed links, ordered after strategy chosen """ # check if user input is valid - if type(city_name) != str: + if type(city_query) != str: raise TypeError("city_name must be a string") if type(proj_crs) != str: raise TypeError("proj_crs must be a string") @@ -67,7 +70,7 @@ def linkbikenet( # fetch street network from OSM g = ox.graph_from_place( - city_name, network_type='all_public', simplify=False, retain_all=True + city_query, network_type='all_public', simplify=False, retain_all=True ) g = ox.simplify_graph( @@ -155,26 +158,30 @@ def linkbikenet( # Generate export data filename if export_data: - os.makedirs("./results/", exist_ok=True) + os.makedirs(settings.export_path, exist_ok=True) + if city_id is None: + city_string = city_query + else: + city_string = city_id export_data_filename = ( - city_name + "." + export_file_format + slugify(city_string) + "-linkbikenet-" + connection_strategy + "." + export_file_format ) if export_data: ### save data print("Saving data..") edges_pbi_gdf.drop(["osmid"], axis=1, inplace=True) - city_boundary = ox.geocoder.geocode_to_gdf(city_name) + city_boundary = ox.geocoder.geocode_to_gdf(city_query) city_boundary.to_crs(epsg=4326, inplace=True) if export_file_format == "geojson": - gdf.to_file("./results/" + export_data_filename, driver="GeoJSON", RFC7946="YES") - edges_pbi_gdf.to_file("./results/" + city_name + "-existing_bike_network.geojson", driver="GeoJSON", RFC7946="YES") - city_boundary.to_file("./results/" + city_name + "-city_boundary.geojson", driver="GeoJSON", RFC7946="YES") + gdf.to_file(settings.export_path + export_data_filename, driver="GeoJSON", RFC7946="YES") + edges_pbi_gdf.to_file(settings.export_path + slugify(city_string) + "-existing_bike_network.geojson", driver="GeoJSON", RFC7946="YES") + city_boundary.to_file(settings.export_path + slugify(city_string) + "-city_boundary.geojson", driver="GeoJSON", RFC7946="YES") elif export_file_format == "gpkg": - gdf.to_file("./results/" + export_data_filename, driver="GPKG", layer="Identified links") - edges_pbi_gdf.to_file("./results/" + export_data_filename, driver="GPKG", layer="Existing bike network", + gdf.to_file(settings.export_path + export_data_filename, driver="GPKG", layer="Identified links") + edges_pbi_gdf.to_file(settings.export_path + export_data_filename, driver="GPKG", layer="Existing bike network", append=True) - city_boundary.to_file("./results/" + export_data_filename, driver="GPKG", layer="City boundary", + city_boundary.to_file(settings.export_path + export_data_filename, driver="GPKG", layer="City boundary", append=True) return gdf \ No newline at end of file diff --git a/linkbikenet/settings.py b/linkbikenet/settings.py index 2894fa8..eb19f36 100644 --- a/linkbikenet/settings.py +++ b/linkbikenet/settings.py @@ -1,4 +1,5 @@ """Global settings for linkbikenet that can be configured by the user.""" import_path = "./" +export_path = "./results/" crs_projected = '3857' \ No newline at end of file From a89ef0fbd11dad4d93a64cca4b1fc2efabfeb3e3 Mon Sep 17 00:00:00 2001 From: Manuel-Knepper Date: Mon, 10 Aug 2026 09:55:55 +0200 Subject: [PATCH 3/5] changed name for connection strategy to largest_closest to fit naming scheme --- linkbikenet/linkbikenet.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/linkbikenet/linkbikenet.py b/linkbikenet/linkbikenet.py index 3bbfa82..aa0ba9e 100644 --- a/linkbikenet/linkbikenet.py +++ b/linkbikenet/linkbikenet.py @@ -22,7 +22,7 @@ def linkbikenet( city_query : str Search string for the city that the analysis should be performed on. This is the query used to fetch the data from nominatim. connection_strategy : str, default="largest - strategy to use for connecting between components. Default is "largest", other options are "largest-closest" and "closest" + strategy to use for connecting between components. Default is "largest", other options are "largest_closest" and "closest" proj_crs : str, default '3857' coordinate reference system that is used to project osm data. Default is '3857' (WGS 84 / Pseudo-Mercator) export_data : bool, optional, default True @@ -49,7 +49,7 @@ def linkbikenet( raise TypeError("city_name must be a string") if type(proj_crs) != str: raise TypeError("proj_crs must be a string") - if connection_strategy != "largest" and connection_strategy != "largest-closest" and connection_strategy != "closest": + if connection_strategy != "largest" and connection_strategy != "largest_closest" and connection_strategy != "closest": raise TypeError("connection_strategy must be 'largest', 'largest-closest' or 'closest'") if type(export_data) is not bool: raise TypeError("export_data must be a boolean") @@ -118,7 +118,7 @@ def linkbikenet( closest_pairs.append(pair) H.add_edge(pair[0], pair[1], length=0) - elif connection_strategy == "largest-closest": + elif connection_strategy == "largest_closest": for i in range(to_iterate): wcc = [H.subgraph(c).copy() for c in sorted(nx.connected_components(H), key=lambda c: sum( [l[-1] for l in H.subgraph(c).copy().edges.data('length')]), reverse=True)] From eb27657de5b123920e4184f7b9c03dcb57846709 Mon Sep 17 00:00:00 2001 From: Manuel-Knepper Date: Mon, 10 Aug 2026 09:58:59 +0200 Subject: [PATCH 4/5] add ordering column --- linkbikenet/linkbikenet.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/linkbikenet/linkbikenet.py b/linkbikenet/linkbikenet.py index aa0ba9e..caa5bc3 100644 --- a/linkbikenet/linkbikenet.py +++ b/linkbikenet/linkbikenet.py @@ -150,6 +150,8 @@ def linkbikenet( df['edge_list'] = df.nodelist.apply(lambda x: get_correct_edgetuples(edges_gdf, x)) gdf = create_gdf_with_geoms(df, edges_gdf) + gdf['ordering'] = gdf.index + edges_pbi_gdf = edges_gdf[edges_gdf["pbi"] == 1] # Back to unprojected (potentially). No more calculations after here. From ec8603b1411db3121ca51deeed231331360837ea Mon Sep 17 00:00:00 2001 From: Manuel-Knepper Date: Mon, 10 Aug 2026 10:31:29 +0200 Subject: [PATCH 5/5] add defaultdict if no importfiles provided --- linkbikenet/linkbikenet.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/linkbikenet/linkbikenet.py b/linkbikenet/linkbikenet.py index caa5bc3..95acb66 100644 --- a/linkbikenet/linkbikenet.py +++ b/linkbikenet/linkbikenet.py @@ -3,6 +3,7 @@ import osmnx as ox import networkx as nx import pandas as pd +from collections import defaultdict from linkbikenet.functions import * @@ -55,6 +56,10 @@ def linkbikenet( raise TypeError("export_data must be a boolean") if export_file_format != "geojson" and export_file_format != "gpkg": raise ValueError("export_file_format must be 'geojson' or 'gpkg'") + if type(import_files) is not dict: + raise TypeError("import_files must be a dictionary") + # Prepare special case import_files. Turn it into a defaultdict where missing keys are None. + import_files = defaultdict(lambda: None, import_files) if import_files['street_network'] is not None: