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/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/examples/development.py b/examples/development.py index 070dcc3..cc226cd 100644 --- a/examples/development.py +++ b/examples/development.py @@ -4,16 +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", - connection_strategy='largest_to_closest', + 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_file_format = "gpkg", + 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/constants.py b/linkbikenet/constants.py index c2627cd..8fd0cd7 100644 --- a/linkbikenet/constants.py +++ b/linkbikenet/constants.py @@ -1,11 +1,16 @@ -"""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 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 3601bae..38afbb2 100644 --- a/linkbikenet/linkbikenet.py +++ b/linkbikenet/linkbikenet.py @@ -1,21 +1,25 @@ +"""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 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 import sys # noqa: F401, use sys.exit() for debugging -from linkbikenet.functions import ( +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, @@ -32,7 +36,6 @@ import_bike_network, assign_pbi_to_edges, mark_joined_component, - resolve_crs_calculations, shortest_path_components, shortest_path_components_from_candidates, slugify, @@ -40,26 +43,27 @@ 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, 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. - connection_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].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 @@ -67,11 +71,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: @@ -102,27 +101,12 @@ def linkbikenet( linked_components : geopandas.GeoDataFrame Geodataframe with the proposed links, ordered after strategy chosen. """ + # Setup starttime = time.time() - - # 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']: - 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 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) - - _print_header(city_query, connection_strategy) + 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 if import_files['city_boundary']: @@ -135,7 +119,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) @@ -147,9 +131,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 @@ -160,12 +141,12 @@ 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 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) @@ -175,13 +156,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) @@ -200,7 +174,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) @@ -286,16 +260,14 @@ def linkbikenet( edges_pbi_gdf['length'] = edges_pbi_gdf['length'].astype(int) 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=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: @@ -305,7 +277,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 + "." + settings.export_file_format ) if export_data: @@ -318,16 +290,25 @@ def linkbikenet( if p not in keepedgedata: del edges_pbi_gdf[p] - city_boundary.to_crs(epsg=4326, inplace=True) - if 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-" + connection_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) - elif export_file_format == "gpkg": + city_boundary.to_crs(epsg=settings.crs_result, inplace=True) + if settings.export_file_format == "geojson": + 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): os.remove(f) # mode="w" does not work for to_file with gpkg. It always appends. Therefore, existing file needs to be deleted. @@ -341,6 +322,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/osm.py b/linkbikenet/osm.py index 26cb2a5..2aea7d4 100644 --- a/linkbikenet/osm.py +++ b/linkbikenet/osm.py @@ -1,22 +1,22 @@ -"""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. -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/linkbikenet/settings.py b/linkbikenet/settings.py index d313605..065c524 100644 --- a/linkbikenet/settings.py +++ b/linkbikenet/settings.py @@ -1,5 +1,14 @@ """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 + 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 @@ -8,7 +17,8 @@ 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/functions.py b/linkbikenet/utils.py similarity index 91% rename from linkbikenet/functions.py rename to linkbikenet/utils.py index bb3af8a..2600ae8 100644 --- a/linkbikenet/functions.py +++ b/linkbikenet/utils.py @@ -1,12 +1,13 @@ -from . import osm -from . import settings -from . import constants +"""Utility functions for `linkbikenet`.""" + +from . import constants, osm, settings import re import osmnx as ox import networkx as nx 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 @@ -14,13 +15,80 @@ 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 _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. """ 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): @@ -30,11 +98,11 @@ 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)*"=") + def initialize_progress_bar(desc_string, total=1, unit="step"): """Initialize tqdm progress bar. """ @@ -46,6 +114,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 @@ -85,6 +154,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 @@ -121,27 +191,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): @@ -209,6 +276,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. @@ -249,12 +317,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 +368,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 +407,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 +446,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"), @@ -519,6 +587,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 @@ -581,6 +650,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. @@ -701,6 +771,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. @@ -774,6 +845,7 @@ def slugify(s): s = s.translate(tab) return s + def calculate_network_statistics(H): """Return total network length and largest component length. @@ -808,6 +880,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. @@ -827,6 +900,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 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] diff --git a/tests/test_data/frederiksberg-linkbikenet-closest_components.gpkg b/tests/test_data/frederiksberg-linkbikenet-closest_components.gpkg index 8d55907..9adc2c2 100644 Binary files a/tests/test_data/frederiksberg-linkbikenet-closest_components.gpkg and b/tests/test_data/frederiksberg-linkbikenet-closest_components.gpkg differ diff --git a/tests/test_data/frederiksberg-linkbikenet-largest_to_closest.gpkg b/tests/test_data/frederiksberg-linkbikenet-largest_to_closest.gpkg index cbc5f43..97ccaeb 100644 Binary files a/tests/test_data/frederiksberg-linkbikenet-largest_to_closest.gpkg and b/tests/test_data/frederiksberg-linkbikenet-largest_to_closest.gpkg differ diff --git a/tests/test_data/frederiksberg-linkbikenet-largest_to_second.gpkg b/tests/test_data/frederiksberg-linkbikenet-largest_to_second.gpkg index 5244b35..b7e9e94 100644 Binary files a/tests/test_data/frederiksberg-linkbikenet-largest_to_second.gpkg and b/tests/test_data/frederiksberg-linkbikenet-largest_to_second.gpkg differ diff --git a/tests/test_linkbikenet.py b/tests/test_linkbikenet.py index d7c7f8f..fc3ef74 100644 --- a/tests/test_linkbikenet.py +++ b/tests/test_linkbikenet.py @@ -3,72 +3,75 @@ from pandas.testing import assert_frame_equal import linkbikenet as lbn -lbn.constants.TOP_CLOSEST_COMPONENTS = 5 +valcols = ['network_length', 'lcc_length', 'link_length', 'num_components_added', 'lcc_share', 'lcc_gain', 'ordering'] @pytest.fixture def validation_gdf_frederiksberg_cc(): gdf = gpd.read_file("./tests/test_data/frederiksberg-linkbikenet-closest_components.gpkg", layer='Identified links') - return gdf[['network_length', 'lcc_length', 'link_length', 'num_components_added', 'lcc_share', 'lcc_gain', 'ordering']] + return gdf -def test_linkbikenet_closest_components_case_success_offline1(validation_gdf_frederiksberg_cc): +def test_linkbikenet_closest_components_success_offline1(validation_gdf_frederiksberg_cc): """Verify that the offline version of fixbikenet, closest_components works as intended. """ + lbn.constants.TOP_CLOSEST_COMPONENTS = 5 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", }, ) assert_frame_equal( - validation_gdf_frederiksberg_cc, - linked_components[['network_length', 'lcc_length', 'link_length', 'num_components_added', 'lcc_share', 'lcc_gain', 'ordering']] + validation_gdf_frederiksberg_cc[valcols], + linked_components[valcols] ) @pytest.fixture def validation_gdf_frederiksberg_l2c(): gdf = gpd.read_file("./tests/test_data/frederiksberg-linkbikenet-largest_to_closest.gpkg", layer='Identified links') - return gdf[['network_length', 'lcc_length', 'link_length', 'num_components_added', 'lcc_share', 'lcc_gain', 'ordering']] + return gdf -def test_linkbikenet_largest_to_closest_case_success_offline1(validation_gdf_frederiksberg_l2c): +def test_linkbikenet_largest_to_closest_success_offline1(validation_gdf_frederiksberg_l2c): """Verify that the offline version of fixbikenet, closest_components works as intended. """ + lbn.constants.TOP_CLOSEST_COMPONENTS = 5 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", }, ) assert_frame_equal( - validation_gdf_frederiksberg_l2c, - linked_components[['network_length', 'lcc_length', 'link_length', 'num_components_added', 'lcc_share', 'lcc_gain', 'ordering']] + validation_gdf_frederiksberg_l2c[valcols], + linked_components[valcols] ) @pytest.fixture def validation_gdf_frederiksberg_l2s(): gdf = gpd.read_file("./tests/test_data/frederiksberg-linkbikenet-largest_to_second.gpkg", layer='Identified links') - return gdf[['network_length', 'lcc_length', 'link_length', 'num_components_added', 'lcc_share', 'lcc_gain', 'ordering']] + return gdf -def test_linkbikenet_largest_to_second_case_success_offline1(validation_gdf_frederiksberg_l2s): +def test_linkbikenet_largest_to_second_success_offline1(validation_gdf_frederiksberg_l2s): """Verify that the offline version of fixbikenet, closest_components works as intended. """ + lbn.constants.TOP_CLOSEST_COMPONENTS = 5 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", }, ) assert_frame_equal( - validation_gdf_frederiksberg_l2s, - linked_components[['network_length', 'lcc_length', 'link_length', 'num_components_added', 'lcc_share', 'lcc_gain', 'ordering']] + validation_gdf_frederiksberg_l2s[valcols], + linked_components[valcols] ) \ No newline at end of file diff --git a/tests/test_functions.py b/tests/test_utils.py similarity index 98% rename from tests/test_functions.py rename to tests/test_utils.py index 536fbbb..5f2e310 100644 --- a/tests/test_functions.py +++ b/tests/test_utils.py @@ -1,6 +1,6 @@ import networkx as nx import pytest -from linkbikenet.functions import * +from linkbikenet.utils import * from linkbikenet import constants, settings constants.TOP_CLOSEST_COMPONENTS = 1