From 8fff7e6fbd1ee99451a84c1614edba184a5abea3 Mon Sep 17 00:00:00 2001 From: Michael Szell Date: Mon, 4 May 2026 11:00:31 +0200 Subject: [PATCH 1/4] Remove web mercator defaults except for top level --- growbikenet/functions.py | 14 +++++++------- growbikenet/growbikenet.py | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/growbikenet/functions.py b/growbikenet/functions.py index 9cef472..a7e55a8 100644 --- a/growbikenet/functions.py +++ b/growbikenet/functions.py @@ -36,8 +36,8 @@ def prepare_network(city_name, proj_crs, network_type='all_public', custom_filte ---------- city_name : str Name of the city that the analysis should be performed on. - proj_crs : str, default '3857' - Coordinate reference system that is used to project osm data. Default is '3857' (WGS 84 / Pseudo-Mercator). + proj_crs : str + Coordinate reference system that is used to project osm data. network_type : {“all”, “all_public”, “bike”, “drive”, “drive_service”, “walk”} What type of street network to retrieve if custom_filter is None. custom_filter : (str | list[str] | None) @@ -63,15 +63,15 @@ def prepare_network(city_name, proj_crs, network_type='all_public', custom_filte nodes, edges = nx_to_nodes_edges(g_undir, proj_crs) return nodes, edges, g_undir -def nx_to_nodes_edges(G, proj_crs='3857'): +def nx_to_nodes_edges(G, proj_crs): """Get nodes and projected edges from networkX graph Parameters ---------- G : networkx.classes.multigraph.MultiGraph networkX graph, undirected - proj_crs : str, default '3857' - Coordinate reference system that is used to project osm data. Default is '3857' (WGS 84 / Pseudo-Mercator). + proj_crs : str + Coordinate reference system that is used to project osm data. Returns ------- @@ -175,7 +175,7 @@ def update_with_existing_bike_network(city_name, proj_crs, g_undir): city_name : str Name of the city that the analysis should be performed on. proj_crs : str - Coordinate reference system that is used to project osm data. Default is '3857' (WGS 84 / Pseudo-Mercator). + Coordinate reference system that is used to project osm data. g_undir : networkx.classes.multigraph.MultiGraph Street network networkX graph, undirected @@ -237,7 +237,7 @@ def update_seed_points_with_existing_bike_network(seed_points_snapped, nodes_exn existing_network_spacing : int Positive integer denoting spacing between seed points, in meters, only on the existing bicycle network. proj_crs : str - Coordinate reference system that is used to project osm data. Default is '3857' (WGS 84 / Pseudo-Mercator). + Coordinate reference system that is used to project osm data. Returns ------- diff --git a/growbikenet/growbikenet.py b/growbikenet/growbikenet.py index b64f088..ac745dd 100644 --- a/growbikenet/growbikenet.py +++ b/growbikenet/growbikenet.py @@ -48,7 +48,7 @@ def growbikenet( city_name : str Name of the city that the analysis should be performed on proj_crs : str, default '3857' - Coordinate reference system that is used to project osm data. Default is '3857' (WGS 84 / Pseudo-Mercator) + Coordinate reference system that is used to project osm data. Default is '3857' (WGS 84 / Pseudo-Mercator). If this web mercator projection is not needed, then '3035' (LAEA) is better for Europe, and '54035' (Equal Earth) is better worldwide. ranking : str, default 'betweenness_centrality' Method used to rank edges. Must be 'betweenness_centrality' (default), 'closeness_centrality', or 'random'. seed_point_type : str, optional, default 'grid' From c945127e41ac1791c5ed73da544f27a3a065413e Mon Sep 17 00:00:00 2001 From: Michael Szell Date: Mon, 4 May 2026 11:15:06 +0200 Subject: [PATCH 2/4] Prepare interfaces for city_boundary_file --- growbikenet/functions.py | 13 +++++++++---- growbikenet/growbikenet.py | 13 +++++++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/growbikenet/functions.py b/growbikenet/functions.py index a7e55a8..95c41f0 100644 --- a/growbikenet/functions.py +++ b/growbikenet/functions.py @@ -28,14 +28,14 @@ def intersects_properly(geom1, geom2): return geom1.intersects(geom2) and not geom1.touches(geom2) -def prepare_network(city_name, proj_crs, network_type='all_public', custom_filter=None, retain_all=True): +def prepare_network(city_name, proj_crs, network_type='all_public', custom_filter=None, retain_all=True, city_boundary_file): """Download and prepare a street network from OSM via OSMnx Downloads a network with a given network_type and custom_filter using ox.graph_from_place. Then, stores the undirected OSM data in gdfs and projects using proj_crs. Parameters ---------- city_name : str - Name of the city that the analysis should be performed on. + Name of the city that the analysis should be performed on. Overruled (for data fetching) if city_boundary_file is set. proj_crs : str Coordinate reference system that is used to project osm data. network_type : {“all”, “all_public”, “bike”, “drive”, “drive_service”, “walk”} @@ -44,6 +44,9 @@ def prepare_network(city_name, proj_crs, network_type='all_public', custom_filte A custom ways filter to be used instead of the network_type presets retain_all : bool, default True If True, return the entire graph even if it is not connected, useful for disconnected bicycle networks. If False, retain only the largest weakly connected component, useful for road networks. + city_boundary_file : (str | None) + If not set to None, the study area will be selected from the (Multi)Polygon provided in the city_boundary_file shape file. For example, "copenhagen.shp". + Returns ------- nodes : geopandas.geodataframe.GeoDataFrame @@ -165,7 +168,7 @@ def get_existing_network_seed_points(nodes_exnw, existing_network_spacing): return seed_points_exnw -def update_with_existing_bike_network(city_name, proj_crs, g_undir): +def update_with_existing_bike_network(city_name, proj_crs, g_undir, city_boundary_file): """Update street network with existing bike network Downloads a network of protected bike infrastructure from OSM (retaining all connected components) and merges it to a given street network graph g_undir. @@ -173,11 +176,13 @@ def update_with_existing_bike_network(city_name, proj_crs, g_undir): Parameters ---------- city_name : str - Name of the city that the analysis should be performed on. + Name of the city that the analysis should be performed on. Overruled (for data fetching) if city_boundary_file is set. proj_crs : str Coordinate reference system that is used to project osm data. g_undir : networkx.classes.multigraph.MultiGraph Street network networkX graph, undirected + city_boundary_file : (str | None) + If not set to None, the study area will be selected from the (Multi)Polygon provided in the city_boundary_file shape file. For example, "copenhagen.shp". Returns ------- diff --git a/growbikenet/growbikenet.py b/growbikenet/growbikenet.py index ac745dd..169ade0 100644 --- a/growbikenet/growbikenet.py +++ b/growbikenet/growbikenet.py @@ -38,6 +38,7 @@ def growbikenet( export_plots=False, export_video=False, allow_edge_overlaps=False, + city_boundary_file=None, ): """Creates a list of edges ordered by a specified ranking method. @@ -46,7 +47,7 @@ def growbikenet( Parameters ---------- city_name : str - Name of the city that the analysis should be performed on + Name of the city that the analysis should be performed on. Overruled (for data fetching) if city_boundary_file is set. proj_crs : str, default '3857' Coordinate reference system that is used to project osm data. Default is '3857' (WGS 84 / Pseudo-Mercator). If this web mercator projection is not needed, then '3035' (LAEA) is better for Europe, and '54035' (Equal Earth) is better worldwide. ranking : str, default 'betweenness_centrality' @@ -62,7 +63,7 @@ def growbikenet( Spacing between seed points, in meters, only on the existing bicycle network. If not set to a positive integer, the existing network is ignored. export_data : bool, optional, default True If set to True, data will be saved to a file. The filename is [slug]-[ranking]-[seed_point_type].gpkg, where slug is a string id made out of city_name - export_data_slug : stri, optional, default None + export_data_slug : str, optional, default None If not set to None, the city_name will be slugified and used as the slug in the filename of the data export export_file_format : str, optional, default "geojson" File format for the data export, relevant if export_data set to True. Default "geojson", also possible "gpkg". If exporting as geojson, generates extra files for seed points and city boundary. If exporting as gkpg, these are added all in one file as extra layers. @@ -72,6 +73,8 @@ def growbikenet( If set to True, video will be saved to a file (only possible if export_plots is set to True) allow_edge_overlaps : bool, default False If set to False, removes edge overlaps in consecutive growth stages. In this case, growth stages that do not add anything new are deleted. + city_boundary_file : str, optional, default None + If not set to None, the study area will be selected from the (Multi)Polygon provided in the city_boundary_file shape file. For example, "copenhagen.shp". Returns ------- @@ -129,6 +132,8 @@ def growbikenet( raise TypeError("export_plots must be a boolean") if type(export_video) is not bool: raise TypeError("export_video must be a boolean") + if city_boundary_file is not None and type(city_boundary_file) is not str: + raise TypeError("city_boundary_file must be None or a string") np.random.seed(42) # Set random number generator seed for reproducibility @@ -137,10 +142,10 @@ def growbikenet( # Fetch street network data from osmnx # Due to retain_all=False, this fetches the largest connected component - nodes, edges, g_undir = prepare_network(city_name, proj_crs, network_type='all_public', retain_all=False) + nodes, edges, g_undir = prepare_network(city_name, proj_crs, network_type='all_public', retain_all=False, city_boundary_file=city_boundary_file) if existing_network_spacing: # TO DO: Check for empty bike infra! - nodes, edges, g_undir, nodes_exnw, edges_exnw = update_with_existing_bike_network(city_name, proj_crs, g_undir) + nodes, edges, g_undir, nodes_exnw, edges_exnw = update_with_existing_bike_network(city_name, proj_crs, g_undir, city_boundary_file=city_boundary_file) ### Create seed points print("Creating " + seed_point_type + " seed points..") From ea0875d1258fdbfda60a818f2ec6289fad547f85 Mon Sep 17 00:00:00 2001 From: Michael Szell Date: Mon, 4 May 2026 12:54:21 +0200 Subject: [PATCH 3/4] Implement city_boundary_file --- growbikenet/functions.py | 25 +++++++++++++++++-------- growbikenet/growbikenet.py | 12 ++++++++---- tests/test_data/copenhagen.dbf | Bin 0 -> 183 bytes tests/test_data/copenhagen.prj | 1 + tests/test_data/copenhagen.qpj | 1 + tests/test_data/copenhagen.shp | Bin 0 -> 25652 bytes tests/test_data/copenhagen.shx | Bin 0 -> 116 bytes 7 files changed, 27 insertions(+), 12 deletions(-) create mode 100644 tests/test_data/copenhagen.dbf create mode 100644 tests/test_data/copenhagen.prj create mode 100644 tests/test_data/copenhagen.qpj create mode 100644 tests/test_data/copenhagen.shp create mode 100644 tests/test_data/copenhagen.shx diff --git a/growbikenet/functions.py b/growbikenet/functions.py index 95c41f0..4422f5a 100644 --- a/growbikenet/functions.py +++ b/growbikenet/functions.py @@ -28,7 +28,7 @@ def intersects_properly(geom1, geom2): return geom1.intersects(geom2) and not geom1.touches(geom2) -def prepare_network(city_name, proj_crs, network_type='all_public', custom_filter=None, retain_all=True, city_boundary_file): +def prepare_network(city_name, proj_crs, network_type='all_public', custom_filter=None, retain_all=True, city_boundary_file=None): """Download and prepare a street network from OSM via OSMnx Downloads a network with a given network_type and custom_filter using ox.graph_from_place. Then, stores the undirected OSM data in gdfs and projects using proj_crs. @@ -44,7 +44,7 @@ def prepare_network(city_name, proj_crs, network_type='all_public', custom_filte A custom ways filter to be used instead of the network_type presets retain_all : bool, default True If True, return the entire graph even if it is not connected, useful for disconnected bicycle networks. If False, retain only the largest weakly connected component, useful for road networks. - city_boundary_file : (str | None) + city_boundary_file : (str | None), default None If not set to None, the study area will be selected from the (Multi)Polygon provided in the city_boundary_file shape file. For example, "copenhagen.shp". Returns @@ -56,10 +56,19 @@ def prepare_network(city_name, proj_crs, network_type='all_public', custom_filte g_undir : networkx.classes.multigraph.MultiGraph Extracted networkX graph, undirected """ + # Fetch street network data from osmnx - g = ox.graph_from_place( - city_name, network_type=network_type, custom_filter=custom_filter, retain_all=retain_all - ) + if city_boundary_file is None: + g = ox.graph_from_place( + city_name, network_type=network_type, custom_filter=custom_filter, retain_all=retain_all + ) + else: + shp = gpd.read_file(city_boundary_file) + city_boundary_polygon = shp.iloc[0].geometry + g = ox.graph_from_polygon( + city_boundary_polygon, network_type=network_type, custom_filter=custom_filter, retain_all=retain_all + ) + g_undir = g.to_undirected().copy() # convert to undirected (dropping OSMnx keys!) # Export osmnx data to gdfs @@ -168,7 +177,7 @@ def get_existing_network_seed_points(nodes_exnw, existing_network_spacing): return seed_points_exnw -def update_with_existing_bike_network(city_name, proj_crs, g_undir, city_boundary_file): +def update_with_existing_bike_network(city_name, proj_crs, g_undir, city_boundary_file=None): """Update street network with existing bike network Downloads a network of protected bike infrastructure from OSM (retaining all connected components) and merges it to a given street network graph g_undir. @@ -181,7 +190,7 @@ def update_with_existing_bike_network(city_name, proj_crs, g_undir, city_boundar Coordinate reference system that is used to project osm data. g_undir : networkx.classes.multigraph.MultiGraph Street network networkX graph, undirected - city_boundary_file : (str | None) + city_boundary_file : (str | None), default None If not set to None, the study area will be selected from the (Multi)Polygon provided in the city_boundary_file shape file. For example, "copenhagen.shp". Returns @@ -210,7 +219,7 @@ def update_with_existing_bike_network(city_name, proj_crs, g_undir, city_boundar ox.settings.useful_tags_way.extend(custom_tag) # Fetch protected bike network data from osmnx # Due to retain_all=True, this fetches all the connected components - nodes_exnw, edges_exnw, g_undir_exnw = prepare_network(city_name, proj_crs, custom_filter=cf, retain_all=True) + nodes_exnw, edges_exnw, g_undir_exnw = prepare_network(city_name, proj_crs, custom_filter=cf, retain_all=True, city_boundary_file=city_boundary_file) g_undir = nx.compose(g_undir_exnw, g_undir) # Merge to be sure we have everything from both # Now we could have some leftover bike infra that is disconnected from the street network and thus not routable. diff --git a/growbikenet/growbikenet.py b/growbikenet/growbikenet.py index 169ade0..d62aa81 100644 --- a/growbikenet/growbikenet.py +++ b/growbikenet/growbikenet.py @@ -49,7 +49,7 @@ def growbikenet( city_name : str Name of the city that the analysis should be performed on. Overruled (for data fetching) if city_boundary_file is set. proj_crs : str, default '3857' - Coordinate reference system that is used to project osm data. Default is '3857' (WGS 84 / Pseudo-Mercator). If this web mercator projection is not needed, then '3035' (LAEA) is better for Europe, and '54035' (Equal Earth) is better worldwide. + Coordinate reference system that is used to project osm data. Default is '3857' (WGS 84 / Pseudo-Mercator). If this web mercator projection is not needed, then for Europe '3035' (LAEA) and globally '54035' (Equal Earth) is better. ranking : str, default 'betweenness_centrality' Method used to rank edges. Must be 'betweenness_centrality' (default), 'closeness_centrality', or 'random'. seed_point_type : str, optional, default 'grid' @@ -73,8 +73,8 @@ def growbikenet( If set to True, video will be saved to a file (only possible if export_plots is set to True) allow_edge_overlaps : bool, default False If set to False, removes edge overlaps in consecutive growth stages. In this case, growth stages that do not add anything new are deleted. - city_boundary_file : str, optional, default None - If not set to None, the study area will be selected from the (Multi)Polygon provided in the city_boundary_file shape file. For example, "copenhagen.shp". + city_boundary_file : (str | None), default None + If not set to None, the study area will be selected from the (Multi)Polygon provided in the city_boundary_file shape file, ideally in unprojected latitude-longitude degrees (EPSG:4326), but EPSG:3857 also works. For example, "./tests/test_data/copenhagen.shp". Returns ------- @@ -275,7 +275,11 @@ def growbikenet( ### save data print("Saving data..") seed_points_snapped.drop(["osmid"], axis=1, inplace=True) - city_boundary = ox.geocoder.geocode_to_gdf(city_name) + if city_boundary_file: + shp = gpd.read_file(city_boundary_file) + city_boundary = shp.iloc[[0]] # This needs to stay a gdf + else: + city_boundary = ox.geocoder.geocode_to_gdf(city_name) city_boundary.to_crs(epsg=proj_crs, inplace=True) # We have meter precision, so rounding to integers is fine. Better would be to # change dtypes to int, but this does not seem possible without manual looping. diff --git a/tests/test_data/copenhagen.dbf b/tests/test_data/copenhagen.dbf new file mode 100644 index 0000000000000000000000000000000000000000..101a0ec0da581afd24d7d117abd304b0075bbe7c GIT binary patch literal 183 zcmZRsVB=(BU|>jO&<2v2Ah9Sl5i05jqD5Iz)fA)_W#*=qq!uwSV3p@pKm`VR28Je< d7N~-TdIlEehUTb(3P7bmV2COT(r;;O2>|Pa6d3>j literal 0 HcmV?d00001 diff --git a/tests/test_data/copenhagen.prj b/tests/test_data/copenhagen.prj new file mode 100644 index 0000000..a30c00a --- /dev/null +++ b/tests/test_data/copenhagen.prj @@ -0,0 +1 @@ +GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]] \ No newline at end of file diff --git a/tests/test_data/copenhagen.qpj b/tests/test_data/copenhagen.qpj new file mode 100644 index 0000000..5fbc831 --- /dev/null +++ b/tests/test_data/copenhagen.qpj @@ -0,0 +1 @@ +GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]] diff --git a/tests/test_data/copenhagen.shp b/tests/test_data/copenhagen.shp new file mode 100644 index 0000000000000000000000000000000000000000..81dc6e61140472d76e5254d6ea3dc27e81df8734 GIT binary patch literal 25652 zcma&uc{EqS!#{9KNLfO*EXlrP$(ke|S&}6}DW!xESxQ7CDI|&(3R#jh$rh!eEM>{Q zYeST5Ws3^=y~po-&-p&*Jbyg?dzra&=g!QXJ2Q9ghl)yK4b}hpr?}xeEfp0V!iuGw z(|!(1Qi_zRTTV9_QS!L1cQF`CQR416tJHlmqWrIi|IdF?Q&DXZ`oE-wh*X>C|Np_7 z^M1oQMpBel^dn8r+Kea@X0$I~=t@y`c%13;hxu$HfwMNH)#D8hda2gxhleJ3)!)}a8QM)(LF6GN>G1)1syCb;OFlOj}KL<(ZcWI zZth(J-%AxKU2HX?oS=Tczi5XPg{#bhdLB-4{3!odQ;Je0vpZ=F{?2@_=R54OmbbSL z{@oltBMw`sm{WDZxM>Z^E!*;(!QHt_3p+6x5F3l-4 z)RmW_B&`)G%Yr%1@2I~52fn<&%?W<_HHzg4ETgI|uLfH&P(F6TOPj{)1>n@-7gQulBGl$y^CT09#j@!FFu7*EW461s=wbZr)buvGLNNHm{YUOpF~5%7>K`6Pd2B!TlA+R`~$7 zzsvEYj!}x@YI@;OEc`Ne_WjA#Qj|ndnscG>`GIY&hvE0%-p*czuYC~ovV{5LnXd)I z@fyh)8t{Mmo$z5c137p>GH{smLv*h9Om@NhX}orj^q4jzyX!IFeqy>NL5|;Ef3|O! z9__K?9yhUY&{y*ec);oD6!C!v&hq1M+T^HXG`!F4=DCNkR&l-!@g>WzUCUHbl;5&_ z$>e_BsqXWu8NL$3x1R?0_p;b3`ZQRL`@#nfc#YTUFO~k)`cIO=_8`(`7i?I%zv~8kn!nxM z6t1{-TlOaW;?u=RE12p2ZEN)9DE6rVATkw>(>)hw$2Oe*`)fBq^_tHpD)HoA%A{-+=#IIMrNbDb=Q>t?szrG>NqNdM`9FUM}s|Mpjs@?72M!dF;xU-PX1*s$i?EPXrr zi|rFJg0PAI&-L7JtlQMK&G4ei!3ToySb!WOJ$!$maibhOwH!D96Zdukj^C-&k>I-Itc_C6E-uG^q`5f1;i!TK>A7`xNo44!i5 zTP%Wy6zfDd;7=QG-+Km6n}oj~pOd7p%L-n44s(^4ccsG#r$f$`!tquc67Rx$j2QHZ zFUlTowuHY^ga+P_JX zr9Is;+QRdnn`0Ofi@gwl` zA>Kd7V2fM*%p_bzW4`JPy!o8*Zhu&UMNd2gzSqTm=L&p4&wcxKxH^S~D-ZtkPPp_w zEKs5N;W=C>RTh*52k!Y+T?K0_`}F3)jLX8#4KP}j>PtB6N`gflyh`M%XeaEko291{ z-uJ5Q&m`Qz9(s5c%HzHaTh>2#NmBQrOr!jVKcFdo>3UKlFz|&N{gD!Hmo> z?7ZQKwIKXZDVMejHnV$MPpspk9@Gm*3mJ88hWGut6Z{?y$R4Sdhcj%i6^+0GVQa4{ zz~(#mO^v~6y3{w6;l-p><*%@5to2VdxWN19hn928XS(haB2U(3^k?+o+b zA=hi4@t|!79BcWs`U6}a?!##gGYe9yeS`g)xYk+2jCD4BUtql}p+a`>o4rR`$n_*C zPV9AukIh~TAA+ZVk%UaAZgNPxwK-!Vh{_{ozpVsxCa=)eo|g`mhtR z-cAqCeh8=~?RB#`cOS8GHPiQgq}TVPnU?`(zUw1&AJ!G*-$h)PP#7LOBS~?(ncBA+ zUbko87z<3zTHweCi`lO$nx01eNA8*8fPdIrDxZaiH})9w!soh2Uv|TundO?q;RRE4 zZLrSAZ{0%h{jI7~)iAp`yPGiVIy&GP0Y@(sq^ZGl2efVk!*?_t#I)hjFCT18;H&(? z?t9>eyC*}H;SUOVwU%%wi|4!~EO`Et(qWjZFgu$MzFT%u!wpt@VSS4Oe$x0oH2^*s zV9Cq`d!1S{5C(VhlsD7B*CWr#UV|%Z#Vy}Wp?w-LJxhesgjdCv!^2wE=4tSUbK3_C zU=F90Yfs_bHUWH(;PVLwGoHgXk7)ffVSet7`7hze--Z6%hASpsU49LJ{GR)68(clO z>wYUd6tHl4X%hEm+5ypzaD3z$vvzoCLz>1QT&i%k%?U2&C_2K_ZAAIm;uv8K>&Z6e zu7f+YWOMexch(wBv%^jkhs;TM^C!l=yl~lzzCufQ;;B!nC~PIT`v-~d_3l8RG_2av z9cBi5+)9{MgnPfaYOuq!Z*^}|;LFC>WWs++QabAFx%b0eD$AA^;XEA`v4ikc@tpH9 za7bLqh2t>YpWa$;xZQ`w%pX20`0kVqTv+q>oEavj7%k3ifVb&8KfeZZ%sfit zgd0!T{<;C5qJQtZ7Jgt~#*hSy$r?#B!mlnLuelG$r+rtWfyZ8UTW7;|8wLy(kiV4t zb3S?Sjy~JcpK#4JHmzc~W@3BnAe`%z%lsUkbGD7|fzu05`<20q!4f;Jz_GvfE;hlB zp7>{pz(0Aaw1;64=|AVwCQ#po;qDB*Mid#p(B}tX<<`hiUijrBIb$PubBdYiMmTc& z@@8XL<4@d1Q8++M<)8taCc*bc5+3J2;%W$Y=q5)g!Y3Z|+3$m04}EsjfK6V~Za)E6 zoN#P2f?MwkTnvXtQZl9-Vb#lLDr4Ze##H`OaKD@SbOKzdb*Rh_PUm3Eh=+?KPGtDQ z!s&sRNcf}Fp>siSMKg!=E!g#CvvC9*Ol$o-80IOd7`hJ!_J+}W!Op|VYoEX?<62mD zM3SKh{ybkgRsvI6__h|ndq+S1%YX%c1)MI0-Hs@5q`+((erhjZ1*WBEsqi-+`T08d zNu*!pQ&_*3O|uWq9+jsnhLeU4NDji=IED)HVYBe)qeF0p*g_<+W=o#=H<+rl=y)D% zvb$^GJKW`~+y4Mg%h%?dfVDrV{lwRtV~-w#%gv|WQ@uB$h;K;imx9?UKK-MHSLRD&l9Yk>Lq@c4@y=bx z8es|b7whO@t<{GML*Q!LgVk$c^#PAJtngLWw?&-rmhkF5m7|gr2Mvr&VYRWHcBkN# z_6Mi-9JgSE;|6}2io+ti`gZ4!pnpv8wc7$mISnk{fos|}d#k|mJ6U43Z7A>>%ItIK3I6h6h87aw=Ed<-?;R3H*EEK zMmq$S`&S}i0GE!0OohV+zb}c)!IlO(-Ocb;rieKKSaC}x=OoO)J@AnemL6a%`v*%2 z-F(Xob2saKXZnEg`k%}-i&*#h_Q+IS9RANbQ12eYc`3N?pw_Nm2w!x3v`YpSdaS*w z0>}5aP+i&$ODh+%&B7rgLY8~r#iGsx29$SV9Nh&65?_CHlqwutEM({c4_0u!I1hVg z8M*qxPZHBNJc7mRnpAJXk1gq|-@_w&)|TeN(K8Q1KfrW8V`F)6X=u6aNBFnj&f|qJ z)mX#bPcY4Q?T8Xs{!*9oXLy(UG}R0E_${lQZLpBJz{}V0Lae$C7uwJ3BF%GEFcbCb z0w;J^`gxwW@R&}cej;q#rvIu2PRL#tRSgSw-zlksRW2C2wxj*l1*JN7z*XToH+JKG z_~fHc^$~vg_-H;K`pc5x@0mZ~TZ>nY7QkGF?UT1&Bd<%L0 zRee53y!W0#CmBEdV_2FZo;|Ikwhi8$$*9T*f8BXC;{ZH-`qF(C_|vwuGDn!P=v68k z9A=@Navd&vaz|tX>}VVQ=sipx%c5}JHyIZ4$8Y<-(Zl7+<-FT9_)p->ME6 zWx$!8W2d&k`v*eojWIrBwQuV)f?sTiS{(qZvmLnY26yR)I)2CaZfdE@_y)}O_`^aT z<~PY+Psh?>hXNDn4VXV2)K`9&2YYq1Piw<3Wp@P@!6^k_jgG*kA>n2va8f0e?_XG3 z*X8{icps&L0uSbWYqWDxV~UoY}i4#D>=^x3^IKhNG|Yi|NOGV#<^!QZW< zX7u1U=l-#-#e5!5e+LTOQy&wt6MmEt-J$_swafB54>ugz;GhOCs$&EKvvYB-l;4I! z-|0h4Y^p9brD3g}^A1ID#mV_-&xEuoUIen`r}TH;tWP*Mzb5hJM9dCo!qY=qyT##tW|jmu`1eu*H>uy$)!83<;iE#iU2-rl z8R)FS`pF%+6IE+h30o z2fdp4_6|;o{_RQHr%q<1=r=fcH)f)+FYgom8Q99el9pV*+P?W(5^ifb?I8SUa7U8*ck+*GyaFrQ z?ep@0?`2VW5&tn167zvKe{fSuN^&JBa*&WO=;!ikT!ehY;YKMNHPVSTE7FgN=P zY}c3lK^f~|&Q&>cf8mD>>H==C>D>s6)mTT@E0*;rf=?I8iE_afsj4rQ;RX3k3PNO^ z{d1-%H`do)CAjx)f-}!f1%8CRw1wHF;ia12-zMO#MVWHMuf808z6>As7I-KHugqhy zUKg}?>nTY%z2W6rLwHSrZh<%)!(Y+83F~`bPgrUQ!oIXzYlPvEo_n9k`E6rcMef6_ z6%vvBu+`H}h6z}Izlbd_Y#|tI%EK&087d0BN3Lh#>ig7FaHF}^ARBzDr^a$E3)W+m zl#@s})9v*Ks8|v1Sp9+>uI5nil!v#{veR!_q0$K$* zq$oz|x=(3g(L2l0f$%*_&C(L;_qtTky*{}A!9nHUuxEvRIL~^lukXC#JqtfMEw3O0 zzhM`$oPlY0C%>z}I-j|>Pr>(=KFaKYS?PMlCt%fh_GL#nA<0o~1P;USz!&~~mW`UU zul1kh^LyaS{Du#n!43n_g`=>yg-2l_Y%>=fvx*b_k8bduTzF9w69#xwSF1!8tfLnp zYz;pu=yOkl>$#sq2f^=JD)Um|v%~QmZ(xJZ>bLL1hnuKQ3vi)*THo0~%)_ylW-mP1 zFTs)mk2lQ39)r)l^64b$_j|bPKM5a`FP(b`%aer$SWx-J50W2>^2|^?JX?G2&0|<( zouy(r?0Si#l{_Ctho>W+!rV1#_4%-SzBpY2oNO0ys{r0z?nb=`U*v1LPpsd6uz?rr z*#$cKdx=BTBP7+}^t>&9$no8^4icx~;5*G(B>giie=1+W(ogK%Nq#z$i(bsZiN{!P zlKj4D&G(YSdU=V-)H8BDZUz%k+OTY4;?GPtKxd%H8%{gz?oRyhSMT9y*n9Qa$_x^J z@=W3)JYKs?n|Qs$^S1)LXm9*V@x%(E4F`4M*NpuYm*2C(yR=0?|+-VF%u@WKy4f-4h4cHsJl$1g|e~ zL4NF1Ivl)^u`dpOFd3CAfcFJe)^G2`!-5;Hghjw-2XC#q1GCA;ok@ou2`aFX_<=$l zDOK>P>g7rjKF-G4H~{%IUG=KNs2Y4xKpoHB2 z2L?}WqY*;>P8_M0g@xp9XEMNjaw#vB;mkmF7H&9Fz5J0DY_i&kS`_}eR3~Kw&;F4V zk%MPw#vfR~lMxv#CUD9+qxG)v%KHyE;Gau%F#I9<{l-hMMXw|8T@ruF!tpk&-Oeua z0A9JD;lJW0mr~*V3CmUjo1`dPO4bdfz(G4y-Miqjf`yTIIL$lnM-hDWQjFzOcy;X^ zvkLe>?`aQbs(!K(mZI2w4UKt?@N1=SD_!AV(%nJ1a1pbbMlQVj>?Wric)RXIZ7O`r zct(k&|1>k6wH)SrV6k`^W@V1p+YOh9?)3|S3*SZ!kHC&^-pP5vr>K_7FNolMnBPMk zci6c1`W_m*zgq5E#oz+#{!B7C06Pli%{jsYTetmghuvQV6xzYVUBwKpcpvsRrShZ= z{IpqGDIMO)rX6tz))eT?sDyvfI;UI1^^epOM&LY~_nrsf;vWiXQsU^}BURX~;9noQ zFM7dBaus{_!vhXJr+nbLKD$v;Uzc?+Fw06{{K9Cveh-`-JQpPm7jw7z7{dnHmnCKq z|8Q9cFL|E6vF@&#hB=CfQ;7fjRjua$+Sk(ZkM>Mhe-G|ZINsmj_h+~! zV{+jp>_V#~U;3A55Fw50j~L`T4p-)$8jphSG2Tfg_NRWNOYF>5xXK>B=(G7yG+a`; zMe`WklXQ?h1@25T__hlc5U>(?0qf51E!2h^ik+$7!>&=1|I}gE8o{v{*eZ8whZ3Cg z+dGyO?@!wr5B??N-^?Zdm`(7H2aNS1aAoa9<{hx7Gv2(zAE|_kPQWhh!sB9ar^mbT zyRdy|K&UwENEQa*i&95k6aUO~y-DoZ!}f*5*AD(tL*h$XA2~v7t~Vv^3YXK>hLZFn z*FL(U1~!vt5#-O z<7PPCr;CFI9xYa8UxdfrUn=gx{LkC*RPidLw~lu~jm#G}Y3i}C!9T`jSli&;Aq64i ze4oNjA83Ksr8Af+!j8!`lFhIM3xB8{Osb^`?rFLg@3~ouvVm<&eGe?WH0Rw9yKbo0 z8i6yJzZ{u`jY9-n$KVaeQ*(d9_KdFf(dkl z(BS$G^!?d*zwekBTm292V;$0a2OAVFZCHjaZJ(C^gBdJ7_bkCLLYpI% zu|J?#e~;$^tgX(s-3AuUjl4e(Pm1=qpMn*fN@U3Sr+7NtwBSnLmlwXkHa%LiyJ0Ky ztqlY4^!ZcShOkZJu-m zEUd%df=674Q`cMwRfCgw9=sxE$z7$b3R``2-%8H^FJ_yYA_@PuNQoe@%{WKTz=aHM}vSXEUj9$;OCh>2OSP-jiWi#kKEd8%*PKf&MGZ zBW=CD6?RM2kR5^_G^e%=!tU3bEC*rj!=Ec=;ih|9Pf2-|oqHESuIFO*S^-kt^%W;+ z$n`ilhbDc78L+L0>=#MUqV^!`GjXv?M>oKZ`a`xK;MA7F9~)t^O!6LXG3Z+shEspg zMiX;&)_fO(cPn8f9>!W!*^2!oeR|=EaECxqg!?^65hH%b$8~TM@veYRl{oW6xe72cQ)pfuf#R(y#{lu2P z9jby?-tWKxucy26;IGkwEu?%MzAS{r!F|IP!le9DZZ`1+!*(u40xM1_ojC!sd@0Q( z@#F7jKK+IJGr6*4v=5FoUS&HCe=yt9L~NGLe%u4@9u`U>*Hb1^?tT(>JTrHhnCG?O z?057(p;AFS-LRNq8Ot|VONEx2*spJ=^B(wPo-rfoFByHWs2IVImJ%mg;VY&whVAGd zAC%>fHNuVq`eH5c>UTXY#L16;95@NzR1mma4^PyniMhbFrCFzlwR*e;CEzPOYm{nX z`fuHDMBus{FD7CcQS7LIxz9HKBI|(@TpwMwz>k#deO|y#V>?Rt;IQe3UIj4S{uAFs z;iW$%w%M?gQNcfH*ws;%E)6zi61}7bi;;mf>=WRUV*pn?JohINrYu?&Sill#D)hvE zJ~GZ7g=^W~KD-BC8}9i=o?nq~>Z?e&Qo!GGcbFyZ8+{TS>23S-30&yT{fnIMvf1rz zPvMN4MzZO!w$hH^R`>)veO@LUeAA}74URlg7)vbap~}<=Z<_S+B2HR(IYW%6mKyPB zOd-=Ll)rC`_3c3TsvbrVu%q+|7hb<*aICUK**F!5(S3^ZYX#-rK3u*ZVmV?+4&j{F@Y+>*R_g|q8+O8didrIfAv zVDeh<9PIt_nT{WP@olU;iC?{Xp!F@>D*WLOsjrIduP*(C{SI=h4S+e?Lk=v##lzQr zkn|2_iWQLkba_uAJBXcH`wx-uuU2Ku#F>|Jre|QQY`u}QZ~*6-E*kU~Oo|-)&%peW zlY4d`{O`RNHu5ktkLY=8IG=VnQWEAq|1i%2J`tkCNSvzEZ;%3KNt&p0!tw($oO$rc z$69h+FyoB#>=W4OqEFa**m%D0=Svu)BX45I{F$ICIOO!MGyz&-j5tGzZGH$J8jKd=z%>BIE0*pH-;AU_rmJu?u#kIBAN?9bFew{f@T5S zer>JY0^HEFtNbZk%(LB?cxAs3?A@rfuOIHa6yQysrz_(J*zTdJB(acx*}@AOO0B^|L^pH3b(@!$K*=Ql#vk0$Bkg_M^1sP6D z%O%+3Sn#V=Mf8%UX4)GWLpl~_3OKVU- z1Ab^SA7cl{u4nPu3U6jo-EbTZ7fd{=4y#;G<#B-zPL6LU@jI)H$H(B9da-0@_=6v2 z018r+y-yF!)xowIH#5GWe7`0!X1#-hU2n-VBYe|qrLmRyK-Hm0aah=87gs03JL;Rp zS8YN5{p;4TVg8_PalB~+;X77S|6zxNW=-Ph5x%ld0RFz{5j2Z%C!gR{G9TH(R=h){?^229;yB6xhTA4yxe6UP(R6{S^V7$?j1HQQ3w#ru#{qN8A%(ZablBfP9IP6i~ zV^;Xrb3NNf*nj*irqq%OzJ7Ig-Ag!P^Nxq~aHdf)-8L1>kG!sM%%D8Ed+deNVRiPZ z6Xg7jX?N@nApUHM-XZcnYt!9-L3Z$6dHL5nVDYShLnPd;>HH-MeD~Sk{n7Bs{wlak zSudy^?tAKL5ejF$=|A=!7HGYjcJ05yJ8rm1--9a?-j|Ofd^sjYJRLqqHc}~LexlD~ zRtl#nC)6B+X^t5uSHc>0J6vpFQQEJEs^OQpwbaHsUXrF8gXua#gL ze1A1MV7NyzIxq#!+0dl_oA~c}Yhou$>1h?zM+iR^R}!r0>egZcS0;z9C+nReM<0*w zg;_P5ddYgGTiwIeYOu3favv#AO`l`MI&cYp(t6^9yw?Jb5L-xXCh_@xDz)3e9$Vrk zGhk19mQTCjP1FzGCBXYqxGsgk+P`xS-G$4j7QT1DyvZDqw_&5cb*6l(xL@y7@Z5lv zv?ekP;ipmJl2Nclmsnf`d|)6pid^r6?TL`LpIP*T6g8m5jfAw3Cq&+|nvds(Di zDg0NFVV=~tQ$wA?E!d^D-Gr1sRcAQo1$fo9I9=lG1O+RQN$=N@w@1|B$Hx`1PhK}^P1?)DRx^WccoEz1Nd5m3JASVPzPN0jV+pUk2Y`>QjYx2S z>r@{4lIx4pn%sFAjytiRZRPsKy~48KS%rfa-y(dn=!0(sY=X`rGY1h3_Xg@faiPXS`(&vZnYxVX*fbi3JapDO{-;!p;w`?2Cu%Kbtpk!>JO-%}MzKDpjNs|6mXS>Ia*dpt~ARRmv`6TA2lmM`L+dJH$|U9%wZV>gU#u7)}H zoYi>+*F2nGQvo0TyM2s!OjdqP11zrT)EVNyS$2c~|y^$fXw zh9_s}KEOo}uz?TuPk5lz3A;{7QLU7(YW?3vm{RN9UW0JPs2jW9!5;dJR&}u1O!5VC zKL6L-9qM6Aib|;$oZ2c-P3p_QQ{;FAJoTpWD5+1jH$%({c%ItHug_l@Z@$r6=7e=Z z|I%zk{jbX#;~vL&K{)w-9VuvAydJ>*YNr~>_>5z4Y*Pz7;%dBOGaP_rV{-jFcYN=C1kdrO^5ww3Mf5*& zNPXlIUu8zi(i@I#D0oS{M>|mU zG&x>ldmlO8zud9(Bz*VNt*{XI{oCX$;!TYE6^NMxGtQI!b%E!EcR0en{ANMq__UE- zf`h2<4uQ_UBz}@4WB3!;^E6{C*`HS#t>DB42OnIdc7avd3pK0J9@wM5ayY|AvWBme zU^}f&MSD2^ZSR>?FkkMIu4C|xxV47`G2T}h4(YdnkI4=inZVDIcRK8X%eDz`I||Q+ zrm$+jlMK_T$6=`n=5hrXyRvRxLwZ!E%m38ju>0rMWy1GvZyi_%b3ROncm*@Iq`M2iGzBGIIk3;H!vh}J`l{B~8Tg{5yaU-U8khHx|2NWm zOB=+y6E2B*=gyD*W%vSZ8mT`Wr@^+J@U0k5KLa?5^Zxn0Fx%I)*2K9+irhAE*WtPm za(&^lcP)rTeb`HM;h8^;Cte_ZA$e<6lHY6E5|>%vXZ!to*22?F-bL}q58v)!PdfPX zR;dPm)bB@Swi~~&fA*{Cn(P~JNBMd`vY(gLpt3v>Zh1Vs?h_ochDH7g{DDsWb34p4 z$b2pcR^^;2Yk{u^M(s<7SNdUiVzsgHOZa+4M}G~R^TIiB4CfDN|I|YE2ir!;eIVtd zOF1)7Y^NN0`!C%7_Yln_d?|ST?=rEcSS$%YA$Ku-6|SfK*+)9!_tPIXvBG9I87+uw z{Litm!xCc`Q&#pP_ekw$hSRS&IgBCv@~h_|jIg;+@|96I|GmBm9bEr9!;AQ(VyRs; z&Uf*25M3uM5^%f32L5+-tcmQOX6SYPxf5QQKfxgZEky_6t%^~L74YJ{hVU@>LjgVj zfD2zxc3*@8U#K2`2A>#?suqXoB*w?RVVy);&SaF&8HWAm*x}mRe}cylPP%Irm{~w0 zs~UFFZd4)r&5LiHt?q^s@8Lxiys|D2=Nugg@P);L9!-({NA64iS>j@$pZP;DtHnt! zvOhhy$Te~p9*drfCtm4);ls9ii^MmarX?rgd2<2obFloz_`&-rPh(hQttNv8yp$)UPu!Qdse+uJz2*AxwQ$jbuH_ws zuWl2vT@BY|o?ME9qoz&^lW?ka6~TJAKcaL6&r-orzn>QnUzBS}qlNQy7mCi|`K0~4 z=ZQJKHwfNx@^dhpZ<@Y#Kiu$9F*X_gtvFYI4E{;M#17*H-EU&nCUEVVKt4N|!Lk2? zDJ;VImMa(*Xs=s+1ZMLSkD^h-{P+ew#DIkohITC?J+EAbPsAdw%;|IRF|t4byBJ?7 zO@q%*%zpNQKXDJJHN#?e-)u~Qdk@IH9z^;ysV^>s!G*$4x8nG(!0T1Ma4f>9 ze(9~`dymJJ{4u`$=HhLpHWnO&I06UWnsmR~r{;}$>@S*gFuhx_F49|A= z!b#gwO03}K$6J!-;rI;Rdu@pSDP6E=3BInDbfyMgNE2202Qvti$KHo$Yw&=8$?MEp zaK@8=v84RNuub+PEO)c@FbzDr{`1unFrBvQ1>#L-*)k+y+0bJ?bg%}UfEy6Z!ob^o*Vj9^bt2M-(jenZ2F~KCn6J zLO#a3J8zxnCHZUEd&6=Pt}NPT~+_}1xp^&Rl&cIkgV5&uZA z#YO{oP0OJH8l*2VVP#|lZ(gS87qG_!!&j+d5DV?G!X z>%Dvcwk;UYCG$mHS|;&>Fp2(C8{?6eqEfo>O8<}f9Pg7cbz)AfO|g>j;JO!7dT?`q zm*{qQJGr6Zn6$Tjd*PUCJYgjM)>eyfD>!kz_B<(HuJ+i=```f8V+Gscqg@+cw!$Uy zDcnC$p3>?Pb1iVaFs=LuJT8&TSOoVt+KYXIxz`^Pyas25rhX;u?a9>#+}1GH4A0em zIMRh}uQ7aU{gOVhTbK-!DtvCX<;ozOaG+o2Ij+z7&AK3czn|GHl+d)|1tRce~ z+!talbO0{dt!F<0C%KQBkp78kdc7nM@^j^Oevk#M8M!>Q9)8x<=ui5y``$W?BZ$97 zD|quh_-L=~_RsJm7LQ00U!rorwjS2jW!*$<>GqM~4Loq?uOG2echQ#Dut0uJ9r3G- zva?s=NUy89R&epEGta}|sgGT!h(#o9bj9HB6TI!@dNzHP=-h$!nLoYy`TRZGw9==sh+hDRi=_q`Xbc8VZJoyIv*7uU25IjZg znM#g7yvQCa06$C6*vAWRc8Pea0&Crn*xilwha|7Vic0VeY>)GVwR?XK$-#%PZErQ4 zm?2#&4Y#kNyvxS@woPRqOC0{9JaQ-kzFix4Q4EfKz;^x$2`3+H!Zl-#V<9lU+BPDV z<2@O530~PR1ha5uM|i+onZ?y4{B7wSz0>fc+V4T4@X$kNJ5Si;W}+MMH7geeH~5Q) zogwk)f%I8p68?60l?beJswQC({lkw|z8}QzHqEf-!g$-vNaAy+)c&x6ca5~YBIY># zQ$`35|9sby#1Bz5%#wx!D+6bz@jaq7%dFW4cy_}J-Cyui34@boaXw>fx$eL2&BD^hFmixxHzqzO={@WizqlU`W48S;4l^_S7IlPG-&`3P zhCBCeyy*&m`Y@Z5dapgP!C;PWAZ(y;j^jJ5K{htPoVS!NmrZv==%v%`baCytWekGVH_Sc((IF2jOE`1@dV6&HM=SJUqR z+-6hTOWH5f)d+)9_>Vp>mjP`0H*()?n01Eoirjw($p`ar=!KdLBUmn9_}@!d_`T93 zi9hk_Tvjc-RD7(I#21P2^3p*6Df7w3Ne5mhE_5*-{<~RPliWW_l0&!aVYv<64vO#z z^5POck+s)=xP!jo`#9_{##^}sepfCNybP~=rvi&BT}ouo#d>YSzPtADKxd;+oF2w= zR7#gu!%F^9Bd-wO#k5Yn0Og&bHsSRauBYa*%!7MnZTKgV--o_ghhR18fux;q6p!Z97PKevt18qoa1P7*_I~(8>G>3KxY#*EXb9$il=fT< zK08o$P#wp6@6)p(-$SQHKOSU+*J^BuCf`fjXR>5Xkn|p1E8u{wikA;Z!79?8h0O5q z`tZm3FlVIU=qi}?%GlYh+i5(xLi_13ok6Ak2@3Ibm<*01-7tx{f^|veW1h62);bgkWIoXmVy}QQGN;c zq9Y@aUbes!=9};#@*sy7CpF!Y;KM@NQ}*zDmrvaf__4lE`f*s)_|fn(e8>1GFLC=e zTV{H+2gX9nMmyMPIPh*0!oO)YDIbH?oGn9g;0fg`Z;rt8X5)p~a6&(2;xL>!q`k2f z$7c^lz9hc~z%%HqGy_Laa)OCh*10kM$p7-E;0JzxAY~%#dOIv<|0la2KGJF~7>xCA zGR^-A+j*2xSHP~(x7xqMInAB>$6+hkOUf#IHPGAHG|E zW}ak zj!%}|((ePOI{W#>;QUre2{-P+Yc40+b;6kqjcTc|Pj_H96Ykd&)@6d3FctaG3(g3$ zG9dS7#8Buq27QdToo9Xo!tOC#Ljv&1K5kezw30~?X1E-wdLG6oWY7>^c~1*-&YuvM zg2NLY2YbQn$^8tGaG1>Vq6`&%3DC(4iH?BRi>K4s3@!1Y3H z^~5Zj54W^-dE50OT-!UTEx4ijf&*qmGA6fX7a-Y$Ztc}zK`Vo zoNLXNNk(~%vv(dM_nYPwi+cxQ`t17LWz^rogVLj>@M(b^y8mG6siSZ9z`UNZH^0Ii zW(R2v;m<{k5yYzhMsm#H{grj&U*Oh#M`;ejOFg1m#4OATcOBqcg((pO@KqMISV!1g z%tNdn)+awe0k8b-8SH(WzJDJa9FwHm2bZ38F4BVy|GF%Z-)q=iJGV{;Uaj;rlAOQp zl<^Bqn0B-=>ks_lfY!JM+$V8B_AlIRWU*>1ys}>f4kRyVVBS4DzAVC`UOoYD(7rqk zI~?cX&q-Y(Rq$Eu>+`d)0$Ff|>1iH&&%if>!VVO{pWN|-4sh@$dd_@U6F&R3UOUof?p>4W!h^SbK?=itZV6057=;?3Eji}3Ru zW%iZu?yDj$OR(XX2P64?i}3}|=yk|{rAD4pIea76S9v4sMLxKJ-JMpw7J%KjwG_$m z&UT#j#Hj~3@`>+xB+fcxJdT^R zsy=|_MRM3k`MbuSy_fo5;WB+n?Dt`n1Yy1h2tREz979|?m2~7XJfL?}J051&dHE^= zHmBCEkAquB{M5K$(+=6#^YDkP?AM~O7n56h0NgKaDl`tOt(KA_;a2*A2kMbOTBXT} zO!!r@_jgm=Z@*4(E|K#I$x5{yfY+Rq^3Q<3eOX`0iu-eJY|q&wn3;UI2~STqJ0-xC z-2v2gaMZl%$5{Bg`BRn?unE8XtvfLPiKFtg2I#Mpc7~Jths`Y1v=8A|v>%=xgQjM@ay2w&JuVq^OQ#*Z2sOyr31coylwI2`+VN=FI8X5SbE54kCw^?`I zybj-2rMpI+pOWMQ9{6onPyHmEG3Xzi2d|rQSw8`*Z{p=Hg8%ZLcut97zvf%PbR5{%tsz467FKu?@rAdt^(};Aeef#iMY9%Hg3T_zY9+5%T*) z(*pO`M!}!hJuIf-&mIS*N%`l#5~Es%1(;;sz1@xZpQYK}U$AW6VUGmZTG?V9iSJU} zYUm1!dj!Ri^!lE4Nvgm%Wpa{9{+WU@FD#>e$*&}k@q|XhbW0n2m*&bT60U>Kb#BAO zR_AKT@fTYuVU}>;=6benc-&2i^9$||UYa2LX1Ht>&2Jjq5BQbJ!|!0}!9J(Mh=26D zvvC{DB*J57hV!p{9}gQOin*D>+rtzs-@taQ<+LX7&!56H)o_*GEd^h=r*X@h3RuDI z*4GPgbh-;y73|TG#~T8RzK|U!@pnX8=Y+x6MYh~~3pZ>|bh!c_6#clN2LAo%ty>g4 z?ELozv8Srj^*Pv!Urw?TZW69<6i4}be|F97fOj!X{gZ>a^s7I2!VmDXWGb-mTh}AR zEH&AY#KxK4d?bE!R!(d)($mouGRc9jvEYRV+%EW)D;r+mJDBtce!0V|D-&Mp)lg*t z>mR1tkPhczS+p4C70n?Y=?HKBG35IYrV*H*w}%t>+BsrjZ&HwO$(PoE%dr38^2jmx z^{xYX=i!^`8s#Kh;r(F98F=M;HCTPV(7+vD*~bXWszf%p5jUrEIKYeJsz*FwvVDtK zOd7v53a@w9=_2knk;;pKpU=wgJ`OYa#vOYAcdHcFko;!x414Uv^{B**W~RXc293O3hT9}8-0z<24DYluILlnwj9LDy4uzJlNHve{$@+i}E| zzJ`Z~Qu@r{KA*+Yq&=)rWmGqWSDr`k$~*{u(6Rn0iQlq5##xkvKS~dJ0>?dc*~$ms zn!~^y-quJn!2*Xx?|DY-5!0}r7GC+z9j4mbnSBS>SN%gHl(d(Y_8{#x^pDc7`M*@d z_41OwdGMAXqeD;OIm0JfIq-A){kMseQfMz;h81kx@0G&VG1o60hVA%_1s}pj?Aww; z;NO3jKauvZ@_U$Y)1Rh}Xt?9e;nk9`YssD7NH`!$*G~bq^*rrx6~29&&V~sl!}Tz@ zL5g|{dEO|pk0ph|^mS8zxZ&i;*evqD;s~=}M zars8KHW7G{do>MdKl5yilM--bd9X4GkC141BL&m`;@?HuYcq?@FERLinRqd2&n{#l z3D=sdL=Xqb^cIlkkxffvlr8LMSZ4DT=ij#TSk6(Hf!*WxR`frMfp}vHt0tW9;DjSS z4Ne_|C6$+xSm4)LKR%Ff?*3C0YPg%Hd4YH|O!&tF`ZKY6r@RlrYEv8em*8+-8UMqu z=(+N>RES@Fjf%qtUdaEPMbanBM8qe9WY=)P%bYrwj=(!krRx~OYv}_-NdEdG&4hj7 z>qB=OiOpJfH4nlo^BGv5YvVpzTn`s{@WPLZig{_^iwE_iNPYQc3yYKSeq&dD45{Dr z;-WuwaK7iwZ&C1(=1tEF;HUnjrsVhWzVCAOB>7Fo=Z>U(Q;{EP#Pv+5Cr{SG=Gh`Q zYT%GGI>9FR-*<|CJjOp^?sE+F@H0KrAZ|EY0Uz|j60ILCe_+12GCqOD$i_if`q_!| zAK+I0e_At`?+NKD4)wrN;i_!AFh8bvDZV#_$>%zc;S6y{yQ8pekC{*^yshC5w=F!Fx_tFMEIWGZ z<1sk;+ML{Nxa$oTY~WYlx1WiCBZ4eD9>UgS;~!k4vHNH)d^*}SDG(lJ<*sjshYR?- zz2TO9t9FmTZu&1}PQg-aciM=(D$W#p!UC_T&yK>1X&oNKu04i2-{GzP1=_^P`y36& zVEh_~n+HsO7Hkq;t<_LUe5}*u;A)hw=)jg~XIM_b`#vo!eRVOJ_;A+!LVDu=*VVlT zL%GIr0563_QM7XFNSoNAi(8_UN-0GmBpRYaE~z%eNeLz8mi95-=7&XXEL*YeCFAG-pjo2c;Dy!J>NGVuG|C8-@fgLGHjjTK7-De z(LBf7DzIM&M%ds_>&DJ9;pz92w~}wW*zVMT->$orL&q=6!_~?dmN8kCYX?_--!iK6bKm1^?BhgRH;nz!SzeK@E*J(C| z`va<7lHrN-5Kd!OFHC{&zRDCagdZxR@EG`9%oWgEK_JxVrF-AfH3!u<^0? zH=8jw^K;lZ4aqXQt1G-KdZNE{qN9p9c?b7weIMt!*#tSAm(T z!Y$NZWSFQj;kff&v*GH^Zez-5A3`|>-H({LJ3J$Mjt0h(wV&@lMIO~rV38&J!nDcL zD*T73KYnv=r4n5CgfC9Z%bwaBMgAJz5+VhA$+~)yweA{gh{MA|1W>>lmF&Lh@O2yg zcCxlSjxbm+dA$wwf4g@ijjYynnMbDhHc{AlxwAH{f9P9}Y9aQItM{H&wEfT75vuL5 z{*Ic%3*kd~X=1f7#XxJqXI~m8?S;jS#+B*&tqT~bRXF~v!_uRuKUo@(g)=dki`Hj# zY~9r(@VLL68ksHU8?qVxOMb9P0T#50){~cIlx`q*-`%`g4^HW9FeHy^-!RdK73K$6 z%!OND>kN^_YS_N=@JhqkqkNq2yW~$h&w*naN_ow&(P@hi8Mr*GBxM`Ee?BDR&(l3!4JYQMUKxca#{J1RHE#~U{4o^5WB<1{Clxc$eg!A4(u%gy0@?i!mP*grk(Ni8(K6T9iMXE&U&?@qTXJa3EP zr!Kfq8VO3U@ql%FC(O3H$MAxAiU*57!)D^BuzsBdkg&7GESB(NEomD0C&C4Wzcxng29k5%G>SZJ<{0@_aAmT zUJOervV2Y8S@s9~JC zrn@^~0D0VzUR&=;!x8rHEBoPFA{jk29x%&8?`97?_1f-&c3Ah$O_i_kiER&E1n|Ui z;T@+Mg+kcg@0X-6@Z@_S{2dES$;Hd9l1I_r< zd5?IXQ7bgCfApy`YUSuX5=?yP@8>$A$={B(xILx1E#-6lJ6@o#n3Wp}ZOIgf2D{Z4 ziBg@@IU|mY1p207-9b25tK2^se#?*!qPk0gYmPe213Fb!96`$-krMp-?tklYPnDC~ z2wNQ3TuAK&MO6VG;h2#>xMYSDp0L7^zUOtwKAyYy zE@?yiTqTows#|1cva?!XZeF)0+3?JikM;1mH4oxw{T6i<&VB_`ya?rbrssVR%!g0Z jq_5$@FF5t}p>T?2&`}n=a~~2$;0J-JbEr=1=l%B|n?5oa literal 0 HcmV?d00001 diff --git a/tests/test_data/copenhagen.shx b/tests/test_data/copenhagen.shx new file mode 100644 index 0000000000000000000000000000000000000000..66e4f92e9e7a1109c43f822f1886a5c89919d36b GIT binary patch literal 116 zcmZQzQ0HR64y;} Date: Mon, 4 May 2026 13:07:54 +0200 Subject: [PATCH 4/4] Polish city_boundary_file verbosity --- growbikenet/growbikenet.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/growbikenet/growbikenet.py b/growbikenet/growbikenet.py index d62aa81..3977864 100644 --- a/growbikenet/growbikenet.py +++ b/growbikenet/growbikenet.py @@ -87,7 +87,8 @@ def growbikenet( .. [2] P. Folco, L. Gauvin, M. Tizzoni, M. Szell, "Data-driven micromobility network planning for demand and safety", Environment and planning B: Urban analytics and city science 50(8), 2087-2102 (2023) """ - # check if user input is valid + + # Check if user input is valid if type(city_name) is not str: raise TypeError("city_name must be a string") if type(proj_crs) is not str: @@ -134,11 +135,17 @@ def growbikenet( raise TypeError("export_video must be a boolean") if city_boundary_file is not None and type(city_boundary_file) is not str: raise TypeError("city_boundary_file must be None or a string") + if type(city_boundary_file) is str and not os.path.isfile(city_boundary_file): + raise FileNotFoundError("city_boundary_file not found") np.random.seed(42) # Set random number generator seed for reproducibility ### Download and preprocess data from OSM - print("Downloading OSM data..") + if city_boundary_file: + import_string = " from city boundary provided in "+city_boundary_file + else: + import_string = "" + print("Downloading OSM data"+import_string+"..") # Fetch street network data from osmnx # Due to retain_all=False, this fetches the largest connected component