From 56b16fa4c2d27564cff77170b3c7cc16f95f6844 Mon Sep 17 00:00:00 2001 From: Michael Szell Date: Wed, 21 Oct 2026 09:27:14 +0200 Subject: [PATCH 1/8] Rename link_strategy and custom filters --- examples/development.py | 8 ++++---- linkbikenet/functions.py | 14 +++++++------- linkbikenet/linkbikenet.py | 20 ++++++++++---------- linkbikenet/osm.py | 24 ++++++++++++------------ tests/test_linkbikenet.py | 6 +++--- 5 files changed, 36 insertions(+), 36 deletions(-) diff --git a/examples/development.py b/examples/development.py index 070dcc3..e25308f 100644 --- a/examples/development.py +++ b/examples/development.py @@ -3,17 +3,17 @@ import linkbikenet as lbn lbn.settings.import_path = '/Users/mszell/Tresorit/bikenetkitshare/' -city_id = "frederiksberg_dk" +city_id = "london_gb" lbn.constants.TOP_CLOSEST_COMPONENTS = 5 edges_ordered = lbn.linkbikenet( - "Frederiksberg", - connection_strategy='largest_to_closest', + "London", + link_strategy='largest_to_second', import_files={ 'city_boundary': 'boundaries/'+city_id+'.geojson', 'street_network': 'streetbike_networks/'+city_id+'.gpkg', }, - export_file_format = "gpkg", + export_file_format = "geojson", ) # print(edges_ordered['network_length']) \ No newline at end of file diff --git a/linkbikenet/functions.py b/linkbikenet/functions.py index bb3af8a..0150f07 100644 --- a/linkbikenet/functions.py +++ b/linkbikenet/functions.py @@ -14,13 +14,13 @@ pd.set_option('display.max_columns', None) # for debugging import sys # noqa: F401, use sys.exit() for debugging -def _print_header(city_query, connection_strategy): +def _print_header(city_query, link_strategy): """Print header. """ if not settings.silent: print((constants._PROGRESS_BAR_DESC_LENGTH+constants._PROGRESS_BAR_LENGTH)*"=") print("RUNNING LINKBIKENET FOR CITY: " + city_query) - print(connection_strategy) + print(link_strategy) print((constants._PROGRESS_BAR_DESC_LENGTH+constants._PROGRESS_BAR_LENGTH)*"-"+"╮") def _print_footer(export_data, endtime, starttime): @@ -249,12 +249,12 @@ def graph_edges_to_gdf(G): return edges_gdf -def link_components(connection_strategy, H, G): +def link_components(link_strategy, H, G): """Link components with the given strategy. Parameters ---------- - connection_strategy : string + link_strategy : string Strategy to use for connecting between components. H : networkx.Graph Graph of bicycle network components. @@ -300,7 +300,7 @@ def link_components(connection_strategy, H, G): paths_all = [] num_comps_added = [] comps_remaining = [] - if connection_strategy == "largest_to_second": + if link_strategy == "largest_to_second": progress_bar = initialize_progress_bar("Linking components", total, "component") for i in range(total): components_sorted = [H.subgraph(c).copy() for c in sorted(nx.connected_components(H), key=lambda c: sum( @@ -339,7 +339,7 @@ def link_components(connection_strategy, H, G): step += 1 progress_bar.close() - elif connection_strategy == "largest_to_closest": + elif link_strategy == "largest_to_closest": for i in tqdm( range(total), desc=("{:<"+str(constants._PROGRESS_BAR_DESC_LENGTH)+"}").format("Linking components"), @@ -378,7 +378,7 @@ def link_components(connection_strategy, H, G): num_comps_added.append(1) step += 1 - elif connection_strategy == "closest_components": + elif link_strategy == "closest_components": for i in tqdm( range(total), desc=("{:<"+str(constants._PROGRESS_BAR_DESC_LENGTH)+"}").format("Linking components"), diff --git a/linkbikenet/linkbikenet.py b/linkbikenet/linkbikenet.py index 3601bae..b662b13 100644 --- a/linkbikenet/linkbikenet.py +++ b/linkbikenet/linkbikenet.py @@ -40,10 +40,10 @@ def linkbikenet( city_query, - connection_strategy = "largest_to_closest", - export_data = True, - city_id = None, - export_file_format = "geojson", + link_strategy="largest_to_closest", + export_data=True, + city_id=None, + export_file_format="geojson", import_files={}, ): """Create links between components of bicycle networks in cities. How @@ -54,7 +54,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 + link_strategy : str, default="largest Strategy to use for connecting between components. Default is "largest_to_second", other options are "largest_to_closest" and "closest_components". export_data : bool, optional, default True If set to True, data will be saved to a file. The filename is @@ -108,7 +108,7 @@ def linkbikenet( # check if user input is valid if type(city_query) != str: raise TypeError("city_name must be a string") - if connection_strategy not in ['largest_to_second', 'largest_to_closest', 'closest_components']: + if link_strategy not in ['largest_to_second', 'largest_to_closest', 'closest_components']: raise TypeError("Connection_strategy must be 'largest_to_second', 'largest_to_closest' or 'closest_components'") if type(export_data) is not bool: raise TypeError("export_data must be a boolean") @@ -122,7 +122,7 @@ def linkbikenet( # Prepare special case import_files. Turn it into a defaultdict where missing keys are None. import_files = defaultdict(lambda: None, import_files) - _print_header(city_query, connection_strategy) + _print_header(city_query, link_strategy) # Get city boundary if import_files['city_boundary']: @@ -200,7 +200,7 @@ def linkbikenet( progress_bar.update(1) progress_bar.close() - H, paths_all, pathedges_all, num_comps_added = link_components(connection_strategy, H, G) + H, paths_all, pathedges_all, num_comps_added = link_components(link_strategy, H, G) progress_bar = initialize_progress_bar("Postprocessing data", 3) edges_pbi_gdf = graph_edges_to_gdf(H) @@ -305,7 +305,7 @@ def linkbikenet( else: city_string = city_id export_data_filename = ( - slugify(city_string) + "-linkbikenet-" + connection_strategy + "." + export_file_format + slugify(city_string) + "-linkbikenet-" + link_strategy + "." + export_file_format ) if export_data: @@ -323,7 +323,7 @@ def linkbikenet( progress_bar = initialize_progress_bar("Exporting data", 3, "file") linked_components.to_file(settings.export_path['results'] + export_data_filename, driver="GeoJSON", RFC7946="YES") progress_bar.update(1) - edges_pbi_gdf.to_file(settings.export_path['results'] + slugify(city_string) + "-linkbikenet-" + connection_strategy + "-existing_bike_network.geojson", driver="GeoJSON", RFC7946="YES") + edges_pbi_gdf.to_file(settings.export_path['results'] + slugify(city_string) + "-linkbikenet-" + link_strategy + "-existing_bike_network.geojson", driver="GeoJSON", RFC7946="YES") progress_bar.update(1) city_boundary.to_file(settings.export_path['results'] + slugify(city_string) + "-linkbikenet-city_boundary.geojson", driver="GeoJSON", RFC7946="YES") progress_bar.update(1) diff --git a/linkbikenet/osm.py b/linkbikenet/osm.py index 26cb2a5..a14cef8 100644 --- a/linkbikenet/osm.py +++ b/linkbikenet/osm.py @@ -1,22 +1,22 @@ """Global constants for `growbikenet` related to OpenStreetMap (OSM). These are meant for advanced users only who have a good understanding of OSM tags. -GROWABLE_NETWORK_CUSTOM_FILTER : list[str] or None - Custom filter for all infrastructure elements that are considered as - growable by `growbikenet`. By default, `growbikenet` uses a custom filter - to retrieve the combined drive and pbi (protected bicycle infrastructure) - network. To only consider the drive network, set - `GROWABLE_NETWORK_CUSTOM_FILTER` to None and `GROWABLE_NETWORK_TYPE` to +CARPBI_NETWORK_CUSTOM_FILTER : list[str] or None + Custom filter for all infrastructure elements that are the combined drive + and pbi (protected bicycle infrastructure) network. By default, + `CARPBI_NETWORK_CUSTOM_FILTER` is a superset of `PBI_CUSTOM_FILTER`, + so it is best to keep them in sync. To only consider the drive network, set + `CARPBI_NETWORK_CUSTOM_FILTER` to None and `CARPBI_NETWORK_TYPE` to 'drive'. However, doing so can lead to issues: https://github.com/BikeNetKit/GrowBikeNet/issues/255. - By default, `GROWABLE_NETWORK_CUSTOM_FILTER` is a superset of + By default, `CARPBI_NETWORK_CUSTOM_FILTER` is a superset of `PBI_CUSTOM_FILTER`, so it is best to keep them in sync. -GROWABLE_NETWORK_TYPE : {'drive', 'all', 'all_public', 'bike', 'drive_service', 'walk'}, default 'drive' - What type of street network to retrieve for the growable network if `GROWABLE_NETWORK_CUSTOM_FILTER` is None. +CARPBI_NETWORK_TYPE : {'drive', 'all', 'all_public', 'bike', 'drive_service', 'walk'}, default 'drive' + What type of street network to retrieve for the car&pbi network if `CARPBI_NETWORK_CUSTOM_FILTER` is None. PBI_CUSTOM_FILTER : list[str] Custom filter for protected bicycle infrastructure (pbi). Always keep in sync with `PBI_DICT` and `PBI_DICT_SUB`. By default, `PBI_CUSTOM_FILTER` is - a subset of `GROWABLE_NETWORK_CUSTOM_FILTER`, so it is best to keep them in + a subset of `CARPBI_NETWORK_CUSTOM_FILTER`, so it is best to keep them in sync. PBI_DICT : dict Dictionary for protected bicycle infrastructure (pbi), for single criteria. @@ -35,7 +35,7 @@ ox.settings.useful_tags_way.extend(custom_tag) -GROWABLE_NETWORK_CUSTOM_FILTER = [ # adapted from https://github.com/gboeing/osmnx/blob/2fc39cb2792ff869881b99f724a2d97f0e958667/osmnx/_overpass.py#L77 +CARPBI_NETWORK_CUSTOM_FILTER = [ # adapted from https://github.com/gboeing/osmnx/blob/2fc39cb2792ff869881b99f724a2d97f0e958667/osmnx/_overpass.py#L77 # Car infra - Instead of excluding what we don't want, we only include what we want. Note: we are OK with alleys and driveways '["highway"~"motorway|trunk|primary|secondary|tertiary|residential|motorway_link|trunk_link|primary_link|secondary_link|tertiary_link|service|unclassified"]["highway"!~"abandoned|construction|no|planned|platform|proposed|raceway|razed|rest_area|services"]["service"!~"emergency_access|parking|parking_aisle|private"]["area"!~"yes"]["access"!~"private"]["motor_vehicle"!~"no"]["motorcar"!~"no"]', # Bike infra, copied from PBI_CUSTOM_FILTER @@ -49,7 +49,7 @@ '["cycleway:both"~"track|opposite_track"]', # opposite_track is deprecated, but could still exist ] -GROWABLE_NETWORK_TYPE = 'drive' +CARPBI_NETWORK_TYPE = 'drive' PBI_CUSTOM_FILTER = [ '["highway"~"cycleway|living_street"]', diff --git a/tests/test_linkbikenet.py b/tests/test_linkbikenet.py index d7c7f8f..3ae0396 100644 --- a/tests/test_linkbikenet.py +++ b/tests/test_linkbikenet.py @@ -16,7 +16,7 @@ def test_linkbikenet_closest_components_case_success_offline1(validation_gdf_fre """ linked_components = lbn.linkbikenet( "Frederiksberg", - connection_strategy='closest_components', + link_strategy='closest_components', import_files={ 'city_boundary': "./tests/test_data/frederiksberg_boundary.geojson", 'street_network': "./tests/test_data/frederiksberg_streetbike_network.gpkg", @@ -39,7 +39,7 @@ def test_linkbikenet_largest_to_closest_case_success_offline1(validation_gdf_fre """ linked_components = lbn.linkbikenet( "Frederiksberg", - connection_strategy='largest_to_closest', + link_strategy='largest_to_closest', import_files={ 'city_boundary': "./tests/test_data/frederiksberg_boundary.geojson", 'street_network': "./tests/test_data/frederiksberg_streetbike_network.gpkg", @@ -62,7 +62,7 @@ def test_linkbikenet_largest_to_second_case_success_offline1(validation_gdf_fred """ linked_components = lbn.linkbikenet( "Frederiksberg", - connection_strategy='largest_to_second', + link_strategy='largest_to_second', import_files={ 'city_boundary': "./tests/test_data/frederiksberg_boundary.geojson", 'street_network': "./tests/test_data/frederiksberg_streetbike_network.gpkg", From 2cfcab901969816a8951b38897d16f390efee1bf Mon Sep 17 00:00:00 2001 From: Michael Szell Date: Wed, 21 Oct 2026 09:33:32 +0200 Subject: [PATCH 2/8] Move export_file_format to settings --- examples/development.py | 6 +++--- linkbikenet/linkbikenet.py | 16 +++++----------- linkbikenet/settings.py | 6 ++++++ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/examples/development.py b/examples/development.py index e25308f..23f3be7 100644 --- a/examples/development.py +++ b/examples/development.py @@ -3,17 +3,17 @@ import linkbikenet as lbn lbn.settings.import_path = '/Users/mszell/Tresorit/bikenetkitshare/' -city_id = "london_gb" +city_id = "frederiksberg_dk" lbn.constants.TOP_CLOSEST_COMPONENTS = 5 +lbn.settings.export_file_format = "geojson" edges_ordered = lbn.linkbikenet( - "London", + "Frederiksberg", link_strategy='largest_to_second', import_files={ 'city_boundary': 'boundaries/'+city_id+'.geojson', 'street_network': 'streetbike_networks/'+city_id+'.gpkg', }, - export_file_format = "geojson", ) # print(edges_ordered['network_length']) \ No newline at end of file diff --git a/linkbikenet/linkbikenet.py b/linkbikenet/linkbikenet.py index b662b13..0894bfb 100644 --- a/linkbikenet/linkbikenet.py +++ b/linkbikenet/linkbikenet.py @@ -43,7 +43,6 @@ def linkbikenet( link_strategy="largest_to_closest", export_data=True, city_id=None, - export_file_format="geojson", import_files={}, ): """Create links between components of bicycle networks in cities. How @@ -67,11 +66,6 @@ def linkbikenet( 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, 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 {} The following key:value entries can be set: @@ -115,8 +109,8 @@ def linkbikenet( if city_id is not None: if type(city_id) is not str: raise TypeError("city_id must be a string") - if export_file_format != "geojson" and export_file_format != "gpkg": - raise ValueError("export_file_format must be 'geojson' or 'gpkg'") + if settings.export_file_format != "geojson" and settings.export_file_format != "gpkg": + raise ValueError("settings.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. @@ -305,7 +299,7 @@ def linkbikenet( else: city_string = city_id export_data_filename = ( - slugify(city_string) + "-linkbikenet-" + link_strategy + "." + export_file_format + slugify(city_string) + "-linkbikenet-" + link_strategy + "." + settings.export_file_format ) if export_data: @@ -319,7 +313,7 @@ def linkbikenet( del edges_pbi_gdf[p] city_boundary.to_crs(epsg=4326, inplace=True) - if export_file_format == "geojson": + if settings.export_file_format == "geojson": progress_bar = initialize_progress_bar("Exporting data", 3, "file") linked_components.to_file(settings.export_path['results'] + export_data_filename, driver="GeoJSON", RFC7946="YES") progress_bar.update(1) @@ -327,7 +321,7 @@ def linkbikenet( progress_bar.update(1) city_boundary.to_file(settings.export_path['results'] + slugify(city_string) + "-linkbikenet-city_boundary.geojson", driver="GeoJSON", RFC7946="YES") progress_bar.update(1) - elif export_file_format == "gpkg": + elif settings.export_file_format == "gpkg": f = settings.export_path['results'] + export_data_filename if os.path.exists(f): os.remove(f) # mode="w" does not work for to_file with gpkg. It always appends. Therefore, existing file needs to be deleted. diff --git a/linkbikenet/settings.py b/linkbikenet/settings.py index d313605..7c2e33e 100644 --- a/linkbikenet/settings.py +++ b/linkbikenet/settings.py @@ -1,5 +1,10 @@ """Global settings for `linkbikenet` that can be configured by the user. +export_file_format : {'gpkg', 'geojson'}, default 'gpkg' + File format for the data export, relevant if `export_data` is set to True. + If exporting as geojson, generates extra files for seed points, city + boundary, and existing bicycle network (if relevant). If exporting as gkpg, + these are added all in one file as extra layers. export_path : dict(str) Paths to results and plots folders to save data and plots. import_path : str @@ -9,6 +14,7 @@ """ crs_projected = 'auto' +export_file_format = 'gpkg' export_path = { "results":"./results/", } From 5c71b3fb930a79451e0e6a92493098b9f2c6a5dd Mon Sep 17 00:00:00 2001 From: Michael Szell Date: Wed, 21 Oct 2026 09:33:44 +0200 Subject: [PATCH 3/8] Add bikenetlib dependency --- environment-dev.yml | 1 + environment.yml | 3 ++- pyproject.toml | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/environment-dev.yml b/environment-dev.yml index ab79ea8..e048747 100644 --- a/environment-dev.yml +++ b/environment-dev.yml @@ -7,6 +7,7 @@ dependencies: - scipy - geojson - tqdm + - bikenetlib # additional to the non-dev env - ipykernel diff --git a/environment.yml b/environment.yml index 3c8b657..b1874f5 100644 --- a/environment.yml +++ b/environment.yml @@ -6,4 +6,5 @@ dependencies: - osmnx>=2 - scipy - geojson - - tqdm \ No newline at end of file + - tqdm + - bikenetlib \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index cf310b6..040e6bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ dependencies = [ "scipy", "geojson", "tqdm", + "bikenetlib", ] [project.urls] From b2e85d27b7b42d66a7bbe35490a935e510ef1e53 Mon Sep 17 00:00:00 2001 From: Michael Szell Date: Wed, 21 Oct 2026 09:39:30 +0200 Subject: [PATCH 4/8] Fix removing export_plots --- examples/development.py | 3 ++- linkbikenet/constants.py | 4 ++-- linkbikenet/functions.py | 1 - 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/development.py b/examples/development.py index 23f3be7..94537eb 100644 --- a/examples/development.py +++ b/examples/development.py @@ -5,7 +5,7 @@ lbn.settings.import_path = '/Users/mszell/Tresorit/bikenetkitshare/' city_id = "frederiksberg_dk" lbn.constants.TOP_CLOSEST_COMPONENTS = 5 -lbn.settings.export_file_format = "geojson" +lbn.settings.export_file_format = "gpkg" edges_ordered = lbn.linkbikenet( "Frederiksberg", @@ -14,6 +14,7 @@ 'city_boundary': 'boundaries/'+city_id+'.geojson', 'street_network': 'streetbike_networks/'+city_id+'.gpkg', }, + export_data = False, ) # print(edges_ordered['network_length']) \ No newline at end of file diff --git a/linkbikenet/constants.py b/linkbikenet/constants.py index c2627cd..dfa9319 100644 --- a/linkbikenet/constants.py +++ b/linkbikenet/constants.py @@ -1,5 +1,5 @@ -"""Global constants for `linkbikenet` that can be tweaked during development, but -should not be changed later by the user. Especially technical or internal +"""Global constants for `linkbikenet` that can be tweaked during development, +but should not be changed later by the user. Especially technical or internal constants start with an underscore. TOP_CLOSEST_COMPONENTS : int, default 5 diff --git a/linkbikenet/functions.py b/linkbikenet/functions.py index 0150f07..f27cb76 100644 --- a/linkbikenet/functions.py +++ b/linkbikenet/functions.py @@ -30,7 +30,6 @@ def _print_footer(export_data, endtime, starttime): print((constants._PROGRESS_BAR_DESC_LENGTH+constants._PROGRESS_BAR_LENGTH)*"-"+"╯") if export_data: print("Data exported to "+settings.export_path['results']) - if export_data or export_plots: print((constants._PROGRESS_BAR_DESC_LENGTH+constants._PROGRESS_BAR_LENGTH)*"-") print("FINISHED IN " + str(datetime.timedelta(seconds = round(endtime - starttime)))) print((constants._PROGRESS_BAR_DESC_LENGTH+constants._PROGRESS_BAR_LENGTH)*"=") From 4a6d275366fdc2c36a52654a6175352c86a6e84c Mon Sep 17 00:00:00 2001 From: Michael Szell Date: Wed, 21 Oct 2026 10:03:46 +0200 Subject: [PATCH 5/8] Rename functions to utils, polish docstrings --- .github/workflows/test.yml | 2 +- docs/source/reference_developer.rst | 6 ++-- docs/source/reference_user.rst | 4 +-- examples/development.py | 12 +++++--- linkbikenet/__init__.py | 2 +- linkbikenet/linkbikenet.py | 9 ++++-- linkbikenet/osm.py | 2 +- linkbikenet/{functions.py => utils.py} | 2 ++ ...ksberg-linkbikenet-closest_components.gpkg | Bin 425984 -> 425984 bytes ...ksberg-linkbikenet-largest_to_closest.gpkg | Bin 425984 -> 425984 bytes ...iksberg-linkbikenet-largest_to_second.gpkg | Bin 425984 -> 425984 bytes tests/test_linkbikenet.py | 29 ++++++++++-------- tests/{test_functions.py => test_utils.py} | 2 +- 13 files changed, 39 insertions(+), 31 deletions(-) rename linkbikenet/{functions.py => utils.py} (99%) rename tests/{test_functions.py => test_utils.py} (98%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eb17e41..e79c352 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -55,7 +55,7 @@ jobs: os: [ ubuntu-latest ] submodule: - { name: "linkbikenet", pytest_args: "tests/test_linkbikenet.py" } - - { name: "functions", pytest_args: "tests/test_functions.py" } + - { name: "utils", pytest_args: "tests/test_utils.py" } fail-fast: false name: ${{ matrix.submodule.name }} (${{ matrix.python-version }} on ${{ matrix.os }}) diff --git a/docs/source/reference_developer.rst b/docs/source/reference_developer.rst index 5651c72..22a92ba 100644 --- a/docs/source/reference_developer.rst +++ b/docs/source/reference_developer.rst @@ -9,10 +9,10 @@ linkbikenet.linkbikenet .. automodule:: linkbikenet.linkbikenet :members: -linkbikenet.functions ---------------------- +linkbikenet.utils +----------------- -.. automodule:: linkbikenet.functions +.. automodule:: linkbikenet.utils :members: :private-members: diff --git a/docs/source/reference_user.rst b/docs/source/reference_user.rst index 3346595..ac0a079 100644 --- a/docs/source/reference_user.rst +++ b/docs/source/reference_user.rst @@ -10,11 +10,9 @@ linkbikenet.linkbikenet .. automodule:: linkbikenet.linkbikenet :members: - :no-index: linkbikenet.settings -------------------- .. automodule:: linkbikenet.settings - :members: - :no-index: \ No newline at end of file + :members: \ No newline at end of file diff --git a/examples/development.py b/examples/development.py index 94537eb..cc226cd 100644 --- a/examples/development.py +++ b/examples/development.py @@ -4,17 +4,19 @@ lbn.settings.import_path = '/Users/mszell/Tresorit/bikenetkitshare/' city_id = "frederiksberg_dk" + +lbn.settings.import_path = './tests/test_data/' lbn.constants.TOP_CLOSEST_COMPONENTS = 5 lbn.settings.export_file_format = "gpkg" -edges_ordered = lbn.linkbikenet( +linked_components = lbn.linkbikenet( "Frederiksberg", - link_strategy='largest_to_second', + link_strategy='closest_components', import_files={ - 'city_boundary': 'boundaries/'+city_id+'.geojson', - 'street_network': 'streetbike_networks/'+city_id+'.gpkg', + 'city_boundary': "frederiksberg_boundary.geojson", + 'street_network': "frederiksberg_streetbike_network.gpkg", }, - export_data = False, + export_data = True, ) # print(edges_ordered['network_length']) \ No newline at end of file diff --git a/linkbikenet/__init__.py b/linkbikenet/__init__.py index 5373e93..4730a14 100644 --- a/linkbikenet/__init__.py +++ b/linkbikenet/__init__.py @@ -15,7 +15,7 @@ from . import linkbikenet from . import osm from . import settings -from . import functions +from . import utils __author__ = "LinkBikeNet contributors" __author_email__ = "michael@szell.net" diff --git a/linkbikenet/linkbikenet.py b/linkbikenet/linkbikenet.py index 0894bfb..8d58b9f 100644 --- a/linkbikenet/linkbikenet.py +++ b/linkbikenet/linkbikenet.py @@ -1,3 +1,5 @@ +"""Main procedure for running `linkbikenet`.""" + # imports from . import settings from . import constants @@ -13,7 +15,7 @@ pd.set_option('display.max_columns', None) # for debugging import sys # noqa: F401, use sys.exit() for debugging -from linkbikenet.functions import ( +from linkbikenet.utils import ( _print_header, _print_footer, calculate_network_statistics, @@ -57,8 +59,9 @@ def linkbikenet( Strategy to use for connecting between components. Default is "largest_to_second", other options are "largest_to_closest" and "closest_components". 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_query. Existing - files are overwritten. + ``[slug]-linkbikenet-[link_strategy].[settings.export_file_format]``, + where ``[slug]`` is a string id made out of `city_query` (or `city_id` + if set). Existing files are overwritten. city_id : None or str, 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 diff --git a/linkbikenet/osm.py b/linkbikenet/osm.py index a14cef8..2aea7d4 100644 --- a/linkbikenet/osm.py +++ b/linkbikenet/osm.py @@ -1,4 +1,4 @@ -"""Global constants for `growbikenet` related to OpenStreetMap (OSM). These are +"""Global constants for `linkbikenet` related to OpenStreetMap (OSM). These are meant for advanced users only who have a good understanding of OSM tags. CARPBI_NETWORK_CUSTOM_FILTER : list[str] or None diff --git a/linkbikenet/functions.py b/linkbikenet/utils.py similarity index 99% rename from linkbikenet/functions.py rename to linkbikenet/utils.py index f27cb76..731a8dc 100644 --- a/linkbikenet/functions.py +++ b/linkbikenet/utils.py @@ -1,3 +1,5 @@ +"""Utility functions for `linkbikenet`.""" + from . import osm from . import settings from . import constants diff --git a/tests/test_data/frederiksberg-linkbikenet-closest_components.gpkg b/tests/test_data/frederiksberg-linkbikenet-closest_components.gpkg index 8d5590700a70a986e5103bc73201adfd7637bcd5..9adc2c2d03f8c7476f319daff82ac53055af8182 100644 GIT binary patch delta 99 zcmZo@kZNd0-ch=IA4iMf@5rJk{|$>gtkS*T)07L)Vz6HvtrEt~J^ Zx8K!g1Y#y2W(HywAZFcuSD#He007SE9>o9v delta 99 zcmZo@kZNd1Y+h@pj*u^ABQS(;c*{;HRSCT2W2Pd@=w%-Fd3u73Mn WeMTT=0%B$$W&vW>?RWLrlmh_Mryke< diff --git a/tests/test_data/frederiksberg-linkbikenet-largest_to_closest.gpkg b/tests/test_data/frederiksberg-linkbikenet-largest_to_closest.gpkg index cbc5f436f06ba4e76b333c4bf7a683e75284365a..97ccaeb6c99465ab4b04f674dafd31ba9766fd80 100644 GIT binary patch delta 98 zcmZo@kZNd0-ch=IA4iJ6s&iJq~A;pDG+S*T*>rjzsZ6ZlZnnKj?h ZZ@;6@2*gZ4%nZaVK+L-Rjy{`8006Za9*6({ delta 98 zcmZo@kZNd1Y+h@pj*v6+>rp`Mw6<>aq=S!iM=lk@Zw_>k3^S~lO& ZZ@;6@2*gZ4%nZaVK+L-Rjy{`8006aR9*O_} diff --git a/tests/test_data/frederiksberg-linkbikenet-largest_to_second.gpkg b/tests/test_data/frederiksberg-linkbikenet-largest_to_second.gpkg index 5244b35e9c80ec5c40b06b42b0b0f154821f2aff..b7e9e94ea7d45a73249c94aa9579cfea9984c328 100644 GIT binary patch delta 98 zcmZo@kZNdUXVkRJF24WTjg`Sz|UXVkRJF24WT Date: Wed, 21 Oct 2026 10:34:30 +0200 Subject: [PATCH 6/8] Add _CRS_CALCULATION, crs_result, validations --- linkbikenet/constants.py | 6 +++ linkbikenet/linkbikenet.py | 62 +++++++++++------------- linkbikenet/settings.py | 6 ++- linkbikenet/utils.py | 99 +++++++++++++++++++++++++++++++++----- 4 files changed, 127 insertions(+), 46 deletions(-) diff --git a/linkbikenet/constants.py b/linkbikenet/constants.py index dfa9319..8fd0cd7 100644 --- a/linkbikenet/constants.py +++ b/linkbikenet/constants.py @@ -6,6 +6,11 @@ The number of top closest components to try to route to, for finding the one with shortest path distance. The higher, the more accurate, but also more computations. +_CRS_CALCULATIONS : str, default 'auto' + EPSG code of the coordinate reference system that is used to project OSM + data for calculations. This has to be a distance-preserving projected CRS, + so '3857' (WGS 84 / Pseudo-Mercator) would be wrong! Option 'auto' selects + the best UTM via `estimate_utm_crs()`. _PROGRESS_BAR_DESC_LENGTH : int, default 21 Character length of tqdm progress bar descriptions. This is the space given to text like "Importing network data ", which is at the maximum of 23 @@ -15,5 +20,6 @@ """ TOP_CLOSEST_COMPONENTS = 5 +_CRS_CALCULATIONS = 'auto' _PROGRESS_BAR_DESC_LENGTH = 23 _PROGRESS_BAR_LENGTH = 23 diff --git a/linkbikenet/linkbikenet.py b/linkbikenet/linkbikenet.py index 8d58b9f..c11d660 100644 --- a/linkbikenet/linkbikenet.py +++ b/linkbikenet/linkbikenet.py @@ -9,7 +9,6 @@ import pandas as pd import geopandas as gpd import warnings -from collections import defaultdict from tqdm import tqdm import time pd.set_option('display.max_columns', None) # for debugging @@ -18,6 +17,10 @@ from linkbikenet.utils import ( _print_header, _print_footer, + _reset_auto_settings, + _resolve_crs_calculations, + _validate_parameters, + _validate_settings, calculate_network_statistics, create_gdf_with_geoms, find_edges_to_drop, @@ -34,7 +37,6 @@ import_bike_network, assign_pbi_to_edges, mark_joined_component, - resolve_crs_calculations, shortest_path_components, shortest_path_components_from_candidates, slugify, @@ -101,24 +103,8 @@ def linkbikenet( """ # Setup starttime = time.time() - - # check if user input is valid - if type(city_query) != str: - raise TypeError("city_name must be a string") - if link_strategy not in ['largest_to_second', 'largest_to_closest', 'closest_components']: - raise TypeError("Connection_strategy must be 'largest_to_second', 'largest_to_closest' or 'closest_components'") - if type(export_data) is not bool: - raise TypeError("export_data must be a boolean") - if city_id is not None: - if type(city_id) is not str: - raise TypeError("city_id must be a string") - if settings.export_file_format != "geojson" and settings.export_file_format != "gpkg": - raise ValueError("settings.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) - + setting_was_auto = _validate_settings() + import_files = _validate_parameters(city_query,link_strategy,export_data,city_id,import_files) _print_header(city_query, link_strategy) # Get city boundary @@ -132,7 +118,7 @@ def linkbikenet( progress_bar = initialize_progress_bar("Importing network data", 1, "network") h = import_bike_network(import_files['bike_network']) nodes_h = ox.graph_to_gdfs(h, nodes=True, edges=False, node_geometry=True) - proj_crs = resolve_crs_calculations(nodes_h, settings.crs_projected) + proj_crs = _resolve_crs_calculations(nodes_h) h = ox.project_graph(h, to_crs=proj_crs) h = nx.Graph(h) @@ -162,7 +148,7 @@ def linkbikenet( # project graph for distance calculations nodes_g = ox.graph_to_gdfs(g, nodes=True, edges=False, node_geometry=True) - proj_crs = resolve_crs_calculations(nodes_g, settings.crs_projected) + proj_crs = _resolve_crs_calculations(nodes_g) g = ox.project_graph(g, to_crs=proj_crs) progress_bar.update(1) @@ -288,11 +274,11 @@ def linkbikenet( #edges_pbi_gdf = edges_gdf[edges_gdf["pbi"] == 1] # Back to unprojected (potentially). No more calculations after here. - linked_components.to_crs(epsg=4326, inplace=True) + linked_components.to_crs(epsg=settings.crs_result, inplace=True) if import_files['bike_network'] is not None: - edges_pbi_gdf.set_crs(epsg=4326, allow_override=True, inplace=True) + edges_pbi_gdf.set_crs(epsg=settings.crs_result, allow_override=True, inplace=True) else: - edges_pbi_gdf.to_crs(epsg=4326, inplace=True) + edges_pbi_gdf.to_crs(epsg=settings.crs_result, inplace=True) # Generate export data filename if export_data: @@ -315,15 +301,24 @@ def linkbikenet( if p not in keepedgedata: del edges_pbi_gdf[p] - city_boundary.to_crs(epsg=4326, inplace=True) + city_boundary.to_crs(epsg=settings.crs_result, inplace=True) if settings.export_file_format == "geojson": - progress_bar = initialize_progress_bar("Exporting data", 3, "file") - linked_components.to_file(settings.export_path['results'] + export_data_filename, driver="GeoJSON", RFC7946="YES") - progress_bar.update(1) - edges_pbi_gdf.to_file(settings.export_path['results'] + slugify(city_string) + "-linkbikenet-" + link_strategy + "-existing_bike_network.geojson", driver="GeoJSON", RFC7946="YES") - progress_bar.update(1) - city_boundary.to_file(settings.export_path['results'] + slugify(city_string) + "-linkbikenet-city_boundary.geojson", driver="GeoJSON", RFC7946="YES") - progress_bar.update(1) + if settings.crs_result == '4326': # Export with RFC7946="YES" + progress_bar = initialize_progress_bar("Exporting data", 3, "file") + linked_components.to_file(settings.export_path['results'] + export_data_filename, driver="GeoJSON", RFC7946="YES") + progress_bar.update(1) + edges_pbi_gdf.to_file(settings.export_path['results'] + slugify(city_string) + "-linkbikenet-" + link_strategy + "-existing_bike_network.geojson", driver="GeoJSON", RFC7946="YES") + progress_bar.update(1) + city_boundary.to_file(settings.export_path['results'] + slugify(city_string) + "-linkbikenet-city_boundary.geojson", driver="GeoJSON", RFC7946="YES") + progress_bar.update(1) + else: + progress_bar = initialize_progress_bar("Exporting data", 3, "file") + linked_components.to_file(settings.export_path['results'] + export_data_filename, driver="GeoJSON") + progress_bar.update(1) + edges_pbi_gdf.to_file(settings.export_path['results'] + slugify(city_string) + "-linkbikenet-" + link_strategy + "-existing_bike_network.geojson", driver="GeoJSON") + progress_bar.update(1) + city_boundary.to_file(settings.export_path['results'] + slugify(city_string) + "-linkbikenet-city_boundary.geojson", driver="GeoJSON") + progress_bar.update(1) elif settings.export_file_format == "gpkg": f = settings.export_path['results'] + export_data_filename if os.path.exists(f): @@ -338,6 +333,7 @@ def linkbikenet( progress_bar.close() # Cleanup, finalize + _reset_auto_settings(setting_was_auto) endtime = time.time() _print_footer(export_data, endtime, starttime) diff --git a/linkbikenet/settings.py b/linkbikenet/settings.py index 7c2e33e..065c524 100644 --- a/linkbikenet/settings.py +++ b/linkbikenet/settings.py @@ -1,5 +1,9 @@ """Global settings for `linkbikenet` that can be configured by the user. +crs_result : str, default '4326' + EPSG code of the coordinate reference system for the resulting geodataframe + and exported data. If '4326' (WGS84) and `export_file_format` is set to + 'geojson', data is exported via the RFC7946 standard. export_file_format : {'gpkg', 'geojson'}, default 'gpkg' File format for the data export, relevant if `export_data` is set to True. If exporting as geojson, generates extra files for seed points, city @@ -13,7 +17,7 @@ If set to True, suppresses all user feedback. Useful for batch exports. """ -crs_projected = 'auto' +crs_result = '4326' export_file_format = 'gpkg' export_path = { "results":"./results/", diff --git a/linkbikenet/utils.py b/linkbikenet/utils.py index 731a8dc..c8ba816 100644 --- a/linkbikenet/utils.py +++ b/linkbikenet/utils.py @@ -9,6 +9,7 @@ import geopandas as gpd import pandas as pd import numpy as np +from collections import defaultdict from scipy.spatial import cKDTree from shapely.geometry import LineString from tqdm import tqdm @@ -16,6 +17,73 @@ pd.set_option('display.max_columns', None) # for debugging import sys # noqa: F401, use sys.exit() for debugging + +def _validate_parameters( + city_query, + link_strategy, + export_data, + city_id, + import_files, + ): + """ Check if user parameter input is valid. If not, raise an exception or + warning. + + Parameters + ---------- + Same as `linkikenet.linkikenet()` + + Returns + ------- + import_files : collections.defaultdict + Defaultdict of checked `import_files`. + """ + + if type(city_query) != str: + raise TypeError("city_name must be a string") + if link_strategy not in ['largest_to_second', 'largest_to_closest', 'closest_components']: + raise TypeError("Connection_strategy must be 'largest_to_second', 'largest_to_closest' or 'closest_components'") + if type(export_data) is not bool: + raise TypeError("export_data must be a boolean") + if city_id is not None: + if type(city_id) is not str: + raise TypeError("city_id must be a string") + 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) + return import_files + + +def _validate_settings(): + """ Check if user settings input is valid. If not, raise an exception or + warning. + + Returns + ------- + setting_was_auto : dict + Dictionary remembering which setting or constant was set to auto, so + it can be reset to auto in the end. + """ + + if type(constants._CRS_CALCULATIONS) is not str: + raise TypeError("constants._CRS_CALCULATIONS must be a string") + if settings.export_file_format not in ["geojson", "gpkg"]: + raise ValueError("settings.export_file_format must be 'geojson' or 'gpkg'") + + setting_was_auto = {'crs_calculations': False} + # Ask whether constants._CRS_CALCULATIONS was 'auto' ahead of network + # construction, as 'auto' is resolved inside `prepare_nodes_edges()`. + if constants._CRS_CALCULATIONS == 'auto': + setting_was_auto['crs_calculations'] = True + return setting_was_auto + +def _reset_auto_settings(setting_was_auto): + """Reset settings and constants to auto. + """ + if setting_was_auto['crs_calculations']: + constants._CRS_CALCULATIONS = 'auto' + + def _print_header(city_query, link_strategy): """Print header. """ @@ -36,6 +104,7 @@ def _print_footer(export_data, endtime, starttime): print("FINISHED IN " + str(datetime.timedelta(seconds = round(endtime - starttime)))) print((constants._PROGRESS_BAR_DESC_LENGTH+constants._PROGRESS_BAR_LENGTH)*"=") + def initialize_progress_bar(desc_string, total=1, unit="step"): """Initialize tqdm progress bar. """ @@ -47,6 +116,7 @@ def initialize_progress_bar(desc_string, total=1, unit="step"): disable=settings.silent, ) + def import_network(street_network): """Import and project a street network from gpkg file @@ -86,6 +156,7 @@ def import_network(street_network): return g + def import_bike_network(bike_network, import_path=settings.import_path): """Import and project a street network from gpkg file @@ -122,27 +193,24 @@ def import_bike_network(bike_network, import_path=settings.import_path): return h -def resolve_crs_calculations(gdf, crs_projected=settings.crs_projected): - """ Resolve settings.crs_projected = 'auto' + +def _resolve_crs_calculations(gdf): + """Resolve constants._CRS_CALCULATIONS = 'auto' Parameters ---------- gdf : geopandas.geodataframe.GeoDataFrame A geodataframe from which to estimate the UTM CRS - crs_projected : str - A given CRS, or 'auto'. If 'auto', it is resolved to an estimated UTM. - In this case, it also sets `settings.crs_projected` to the UTM. Returns ------- - crs_projected : str - If it was set to 'auto', the estimated UTM, otherwise identical to the - input `crs_calculations`. + constants._CRS_CALCULATIONS : str + If it was set to 'auto', the estimated UTM, otherwise identical to + `constants._CRS_CALCULATIONS`. """ - if crs_projected == 'auto': - crs_projected = gdf.estimate_utm_crs() - settings.crs_projected = crs_projected - return crs_projected + if constants._CRS_CALCULATIONS == 'auto': + constants._CRS_CALCULATIONS = gdf.estimate_utm_crs() + return constants._CRS_CALCULATIONS def assign_pbi_to_edges(g): @@ -210,6 +278,7 @@ def find_edges_to_drop(g): edges_to_drop = list(set(edges_to_drop)) return edges_to_drop + def graph_edges_to_gdf(G): """Create a geodataframe with edges attributes from a simple nx graph. @@ -520,6 +589,7 @@ def get_underway_connections(H, pairinfo, components_sorted): connection_points.append(node) return components_connected_underway, connection_points + def pair_between_largest_and_closest_components(wcc): """Find the top `constants.TOP_CLOSEST_COMPONENTS` pairs of nodes connecting the largest component to the geographically nearest remaining @@ -582,6 +652,7 @@ def pair_between_largest_and_closest_components(wcc): return closest_pairs return closest_pairs.nsmallest(constants.TOP_CLOSEST_COMPONENTS,'distance_eucl') + def pair_between_closest_components(wcc): """Find the `constants.TOP_CLOSEST_COMPONENTS` closest pairs of nodes belonging to two different connected components. @@ -702,6 +773,7 @@ def get_correct_edgetuples(edge_gdf, nodelist): edgelist_final.append(tuple([edge_prelim[1], edge_prelim[0]])) return edgelist_final + def path_to_edges(nodelist): """Turn a list of nodes along a path into a list of edges along the path. @@ -775,6 +847,7 @@ def slugify(s): s = s.translate(tab) return s + def calculate_network_statistics(H): """Return total network length and largest component length. @@ -809,6 +882,7 @@ def calculate_network_statistics(H): return total_length, largest_length + def mark_joined_component(H, component, step): """Mark components when they join the largest connected component. @@ -828,6 +902,7 @@ def mark_joined_component(H, component, step): if "lcc_step" not in H[u][v]: H[u][v]["lcc_step"] = step + def shortest_path_components(G, pair_components, path): """Starting from an initial shortest path between a pair of nodes in two different components, identify the two nodes and their shortest path which From be383ad5fb23ee9a046965abd8cd6d794c460eb1 Mon Sep 17 00:00:00 2001 From: Michael Szell Date: Wed, 21 Oct 2026 10:38:59 +0200 Subject: [PATCH 7/8] Clean up imports, code comments --- linkbikenet/linkbikenet.py | 17 ++--------------- linkbikenet/utils.py | 4 +--- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/linkbikenet/linkbikenet.py b/linkbikenet/linkbikenet.py index c11d660..b6a1972 100644 --- a/linkbikenet/linkbikenet.py +++ b/linkbikenet/linkbikenet.py @@ -1,8 +1,7 @@ """Main procedure for running `linkbikenet`.""" # imports -from . import settings -from . import constants +from . import constants, osm, settings import os import osmnx as ox import networkx as nx @@ -130,9 +129,6 @@ def linkbikenet( ### downloading and preprocessing data from OSM progress_bar = initialize_progress_bar("Downloading OSM data", 1, "network") - ox.settings.useful_tags_way = ["highway", "cycleway", "cycleway:right", "cycleway:left", "cycleway:both", - "cyclestreet"] - # fetch street network from OSM g = ox.graph_from_place( city_query, network_type='all_public', simplify=False, retain_all=True @@ -143,7 +139,7 @@ def linkbikenet( progress_bar = initialize_progress_bar("Processing network", 3) g = ox.simplify_graph( g, - edge_attrs_differ=['cycleway', 'highway', 'cycleway:right', 'cycleway:left', 'cycleway:both'], + edge_attrs_differ=['cycleway', 'highway', 'cycleway:right', 'cycleway:left', 'cycleway:both'], # To do: Align with osm module ) # project graph for distance calculations @@ -158,13 +154,6 @@ def linkbikenet( progress_bar.update(1) - # progress_bar = initialize_progress_bar("Drop parallel edges") - # # finding parallel edges and dropping them - # edges_to_drop = find_edges_to_drop(g) - # g.remove_edges_from(edges_to_drop) - # progress_bar.update(1) - # progress_bar.close() - # Capital-G: the Graph() object we will be working with from now on G = nx.Graph(g) @@ -271,8 +260,6 @@ def linkbikenet( linked_components['ordering'] = linked_components.index - #edges_pbi_gdf = edges_gdf[edges_gdf["pbi"] == 1] - # Back to unprojected (potentially). No more calculations after here. linked_components.to_crs(epsg=settings.crs_result, inplace=True) if import_files['bike_network'] is not None: diff --git a/linkbikenet/utils.py b/linkbikenet/utils.py index c8ba816..2600ae8 100644 --- a/linkbikenet/utils.py +++ b/linkbikenet/utils.py @@ -1,8 +1,6 @@ """Utility functions for `linkbikenet`.""" -from . import osm -from . import settings -from . import constants +from . import constants, osm, settings import re import osmnx as ox import networkx as nx From 16a973fb79186840a512bcfadc01caecc5ee245c Mon Sep 17 00:00:00 2001 From: Michael Szell Date: Wed, 21 Oct 2026 10:45:32 +0200 Subject: [PATCH 8/8] Polish docstring --- linkbikenet/linkbikenet.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/linkbikenet/linkbikenet.py b/linkbikenet/linkbikenet.py index b6a1972..38afbb2 100644 --- a/linkbikenet/linkbikenet.py +++ b/linkbikenet/linkbikenet.py @@ -43,22 +43,23 @@ def linkbikenet( city_query, - link_strategy="largest_to_closest", + link_strategy='largest_to_closest', export_data=True, city_id=None, import_files={}, ): """Create links between components of bicycle networks in cities. How - components are connected depends on the chosen connection strategy. + components are connected depends on the chosen link strategy. Parameters ---------- 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. - link_strategy : str, default="largest - Strategy to use for connecting between components. Default is "largest_to_second", other options are "largest_to_closest" and "closest_components". - export_data : bool, optional, default True + link_strategy : {'largest_to_closest', 'largest_to_second', 'closest_components'} + Strategy for linking components. Default is 'largest_to_closest', other + options are 'largest_to_second' and 'closest_components'. + export_data : bool, default True If set to True, data will be saved to a file. The filename is ``[slug]-linkbikenet-[link_strategy].[settings.export_file_format]``, where ``[slug]`` is a string id made out of `city_query` (or `city_id` @@ -100,6 +101,7 @@ def linkbikenet( linked_components : geopandas.GeoDataFrame Geodataframe with the proposed links, ordered after strategy chosen. """ + # Setup starttime = time.time() setting_was_auto = _validate_settings() @@ -258,7 +260,7 @@ def linkbikenet( edges_pbi_gdf['length'] = edges_pbi_gdf['length'].astype(int) linked_components['ordering'] = linked_components.index - + # Back to unprojected (potentially). No more calculations after here. linked_components.to_crs(epsg=settings.crs_result, inplace=True)