diff --git a/examples/development.py b/examples/development.py index 62b1c75..070dcc3 100644 --- a/examples/development.py +++ b/examples/development.py @@ -8,7 +8,7 @@ edges_ordered = lbn.linkbikenet( "Frederiksberg", - connection_strategy='closest_components', + connection_strategy='largest_to_closest', import_files={ 'city_boundary': 'boundaries/'+city_id+'.geojson', 'street_network': 'streetbike_networks/'+city_id+'.gpkg', @@ -16,4 +16,4 @@ export_file_format = "gpkg", ) -print(edges_ordered['network_length']) \ No newline at end of file +# print(edges_ordered['network_length']) \ No newline at end of file diff --git a/linkbikenet/__init__.py b/linkbikenet/__init__.py index c2b75a6..5373e93 100644 --- a/linkbikenet/__init__.py +++ b/linkbikenet/__init__.py @@ -1,12 +1,24 @@ +""" +LinkBikeNet is a Python package to find links between the disconnected +components in a city’s bicycle network. + +Full documentation at: https://docs.bikenetkit.org/LinkBikeNet/ + +If you use LinkBikeNet, please cite: https://doi.org/10.1098/rsos.201130 +""" + import contextlib from importlib.metadata import PackageNotFoundError, version -from . import config -from . import functions from linkbikenet.linkbikenet import linkbikenet +from . import constants +from . import linkbikenet +from . import osm +from . import settings +from . import functions -__author__ = "MS, MK" -__author_email__ = "email@domain.com" +__author__ = "LinkBikeNet contributors" +__author_email__ = "michael@szell.net" with contextlib.suppress(PackageNotFoundError): __version__ = version("linkbikenet") \ No newline at end of file diff --git a/linkbikenet/config.py b/linkbikenet/config.py deleted file mode 100644 index 5918641..0000000 --- a/linkbikenet/config.py +++ /dev/null @@ -1,35 +0,0 @@ -cycleway_bike_infra = [ - 'track' - ] - -cycleway_right_bike_infra = [ - 'track', - 'opposite_track' -] - -cycleway_left_bike_infra = [ - 'track', - 'opposite_track' -] - -cycleway_both_bike_infra = [ - 'track', - 'opposite_track' -] - -highway_bike_infra = [ - 'cycleway', - 'living_street' -] - -highway_bike_infra_extended = [ - 'path', - 'pedestrian', - 'footway' -] - -bicycle_bike_infra = [ - 'designated', - 'yes', - 'permissive' -] \ No newline at end of file diff --git a/linkbikenet/functions.py b/linkbikenet/functions.py index 51a83bb..bb3af8a 100644 --- a/linkbikenet/functions.py +++ b/linkbikenet/functions.py @@ -1,4 +1,4 @@ -from . import config +from . import osm from . import settings from . import constants import re @@ -143,29 +143,36 @@ def resolve_crs_calculations(gdf, crs_projected=settings.crs_projected): settings.crs_projected = crs_projected return crs_projected -def map_edges_to_bike_infrastructure(g): - """ - map if edges in graph have bike infrastructure as specified in config.py + +def assign_pbi_to_edges(g): + """Assign a boolean pbi attribute to all edges in a graph depending if they + are considered protected bike infrastructure (pbi). Parameters ---------- - g :networkx.MultiDiGraph - simplified graph representing the street network + g : networkx.MultiDiGraph + Simplified graph representing the street network. Returns ------- g : networkx.MultiDiGraph - simplified graph representing the street network, with added binary edge attribute "pbi" + Simplified graph representing the street network, with added binary edge attribute "pbi". """ - - # add binary edge attribute "pbi" (protected bike infra: True/False) for edge in g.edges(keys=True): - if g.edges[edge].get("cycleway") in config.cycleway_bike_infra or g.edges[edge].get("cycleway:right") in config.cycleway_right_bike_infra or g.edges[edge].get("cycleway:left") in config.cycleway_left_bike_infra or g.edges[edge].get("cycleway:both") in config.cycleway_both_bike_infra or g.edges[edge].get("highway") in config.highway_bike_infra or g.edges[edge].get("cyclestreet") or g.edges[edge].get("bicycle_road") or g.edges[edge].get("highway") in config.highway_bike_infra_extended and g.edges[edge].get("bicycle") in config.bicycle_bike_infra and g.edges[edge].get("access") != 'private': - g.edges[edge]["pbi"] = 1 - else: - g.edges[edge]["pbi"] = 0 + assigned_pbi = 0 + for tag in osm.PBI_DICT: + if g.edges[edge].get(tag) in osm.PBI_DICT[tag]: + assigned_pbi = 1 + if not assigned_pbi: # Check if the edge fulfills a subcriterion + for tag in osm.PBI_DICT_SUB: + subtag = list(osm.PBI_DICT_SUB[tag][1].keys())[0] + if g.edges[edge].get(tag) in osm.PBI_DICT_SUB[tag][0] and g.edges[edge].get(subtag) in osm.PBI_DICT_SUB[tag][1][subtag] and g.edges[edge].get("access") != 'private': + assigned_pbi = 1 + g.edges[edge]["pbi"] = assigned_pbi + return g + def find_edges_to_drop(g): """ find parallel edges that have different pbi values, list the ones with pbi=0 diff --git a/linkbikenet/linkbikenet.py b/linkbikenet/linkbikenet.py index 9d800ee..42ca741 100644 --- a/linkbikenet/linkbikenet.py +++ b/linkbikenet/linkbikenet.py @@ -30,7 +30,7 @@ initialize_progress_bar, import_network, import_bike_network, - map_edges_to_bike_infrastructure, + assign_pbi_to_edges, mark_joined_component, resolve_crs_calculations, shortest_path_components, @@ -70,7 +70,8 @@ def linkbikenet( 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. + exporting as gkpg, these are added all in one file as extra layers. + Existing files are overwritten. import_files: dict, default {} The following key:value entries can be set: @@ -170,7 +171,7 @@ def linkbikenet( progress_bar.update(1) # check which edges have existing bicycle infrastructure and assign "pbi = 1" to them, all other edges get "pbi = 0". - g = map_edges_to_bike_infrastructure(g) + g = assign_pbi_to_edges(g) progress_bar.update(1) @@ -327,11 +328,14 @@ def linkbikenet( 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": + 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. progress_bar = initialize_progress_bar("Exporting data", 1, "file") - linked_components.to_file(settings.export_path['results'] + export_data_filename, driver="GPKG", layer="Identified links") - edges_pbi_gdf.to_file(settings.export_path['results'] + export_data_filename, driver="GPKG", layer="Existing bike network", + linked_components.to_file(f, driver="GPKG", layer="Identified links") + edges_pbi_gdf.to_file(f, driver="GPKG", layer="Existing bike network", append=True) - city_boundary.to_file(settings.export_path['results'] + export_data_filename, driver="GPKG", layer="City boundary", + city_boundary.to_file(f, driver="GPKG", layer="City boundary", append=True) progress_bar.update(1) progress_bar.close() diff --git a/linkbikenet/osm.py b/linkbikenet/osm.py new file mode 100644 index 0000000..26cb2a5 --- /dev/null +++ b/linkbikenet/osm.py @@ -0,0 +1,80 @@ +"""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 + '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 + `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. +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 + sync. +PBI_DICT : dict + Dictionary for protected bicycle infrastructure (pbi), for single criteria. + Always keep in sync with `PBI_CUSTOM_FILTER`. +PBI_DICT_SUB : dict + Dictionary for protected bicycle infrastructure (pbi), with one + sub-criterion. For example, highway~path AND bicycle~designated. Always + adds a second criterion access!~private. Always keep in sync with + `PBI_CUSTOM_FILTER`. +""" + +# Populate ox.settings.useful_tags_way to make application of custom filter possible +import osmnx as ox +for custom_tag in ["highway", "cycleway", "bicycle", "cycleway:right", "cycleway:left", "cycleway:both", "cyclestreet", "access", "area", "service", "motor_vehicle", "motorcar"]: # This list should contain all tags used in any custom filters + if custom_tag not in ox.settings.useful_tags_way: + 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 + # 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 + '["highway"~"cycleway|living_street"]', + '["highway"~"path|pedestrian|footway"]["bicycle"~"designated|yes|permissive"]["access"!~"private"]', + '["cyclestreet"~"yes"]', + '["bicycle_road"~"yes"]', + '["cycleway"~"track"]', + '["cycleway:right"~"track|opposite_track"]', # opposite_track is deprecated, but could still exist + '["cycleway:left"~"track|opposite_track"]', # opposite_track is deprecated, but could still exist + '["cycleway:both"~"track|opposite_track"]', # opposite_track is deprecated, but could still exist +] + +GROWABLE_NETWORK_TYPE = 'drive' + +PBI_CUSTOM_FILTER = [ + '["highway"~"cycleway|living_street"]', + '["highway"~"path|pedestrian|footway"]["bicycle"~"designated|yes|permissive"]["access"!~"private"]', + '["cyclestreet"~"yes"]', + '["bicycle_road"~"yes"]', + '["cycleway"~"track"]', + '["cycleway:right"~"track|opposite_track"]', # opposite_track is deprecated, but could still exist + '["cycleway:left"~"track|opposite_track"]', # opposite_track is deprecated, but could still exist + '["cycleway:both"~"track|opposite_track"]', # opposite_track is deprecated, but could still exist +] + +PBI_DICT = { + 'highway': ['cycleway','living_street'], + 'cyclestreet': ['yes'], + 'bicycle_road': ['yes'], + 'cycleway': ['track'], + 'cycleway:right': ['track','opposite_track'], + 'cycleway:left': ['track','opposite_track'], + 'cycleway:both': ['track','opposite_track'], +} + +PBI_DICT_SUB = { + 'highway': [ + ['path','pedestrian','footway'], + {'bicycle': ['designated','yes','permissive']}, + ] +} diff --git a/tests/test_data/frederiksberg-linkbikenet-closest_components.gpkg b/tests/test_data/frederiksberg-linkbikenet-closest_components.gpkg index 825441d..8d55907 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 363c7c9..cbc5f43 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 be0f922..5244b35 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_data/frederiksberg_streetbike_network.gpkg b/tests/test_data/frederiksberg_streetbike_network.gpkg index 3b85346..b2d5eca 100644 Binary files a/tests/test_data/frederiksberg_streetbike_network.gpkg and b/tests/test_data/frederiksberg_streetbike_network.gpkg differ