iduedu 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
iduedu/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """
2
+ IduEdu
3
+ ========
4
+
5
+ IduEdu is a Python package for the creation and manipulation of complex city networks from OpenStreetMap.
6
+
7
+ Homepage https://github.com/DDonnyy/IduEdu.
8
+ """
9
+ from ._config import config
10
+ from ._api import *
11
+ from ._version import VERSION as __version__
iduedu/_api.py ADDED
@@ -0,0 +1,7 @@
1
+ # pylint: disable=unused-import
2
+ from .modules.downloaders import get_boundary
3
+ from .modules.drive_walk_builder import get_drive_graph, get_walk_graph
4
+ from .modules.intermodal_builder import get_intermodal_graph
5
+ from .modules.matrix_builder import get_adj_matrix_gdf_to_gdf
6
+ from .modules.pt_walk_joiner import join_pt_walk_graph
7
+ from .modules.public_transport_builder import get_all_public_transport_graph, get_single_public_transport_graph
iduedu/_config.py ADDED
@@ -0,0 +1,58 @@
1
+ import sys
2
+ from typing import Literal
3
+
4
+ from loguru import logger
5
+
6
+
7
+ class Config:
8
+ """
9
+ A configuration class to manage global settings for the application, such as Overpass API URL, timeouts, and logging options.
10
+
11
+ Attributes
12
+ ----------
13
+ overpass_url : str
14
+ URL for accessing the Overpass API. Defaults to "http://lz4.overpass-api.de/api/interpreter".
15
+ timeout : int or None
16
+ Timeout in seconds for API requests. If None, no timeout is applied.
17
+ enable_tqdm_bar : bool
18
+ Enables or disables progress bars (via tqdm). Defaults to True.
19
+ logger : Logger
20
+ Logging instance to handle application logging.
21
+
22
+ Methods
23
+ -------
24
+ change_logger_lvl(lvl: Literal["TRACE", "DEBUG", "INFO", "WARN", "ERROR"])
25
+ Changes the logging level to the specified value.
26
+ set_overpass_url(url: str)
27
+ Sets a new Overpass API URL.
28
+ set_timeout(timeout: int)
29
+ Sets the timeout for API requests.
30
+ set_enable_tqdm(enable: bool)
31
+ Enables or disables progress bars in the application.
32
+ """
33
+ def __init__(
34
+ self,
35
+ overpass_url="http://lz4.overpass-api.de/api/interpreter",
36
+ timeout=None,
37
+ enable_tqdm_bar=True,
38
+ ):
39
+ self.overpass_url = overpass_url
40
+ self.timeout = timeout
41
+ self.enable_tqdm_bar = enable_tqdm_bar
42
+ self.logger = logger
43
+
44
+ def change_logger_lvl(self, lvl: Literal["TRACE", "DEBUG", "INFO", "WARN", "ERROR"]):
45
+ self.logger.remove()
46
+ self.logger.add(sys.stderr, level=lvl)
47
+
48
+ def set_overpass_url(self, url: str):
49
+ self.overpass_url = url
50
+
51
+ def set_timeout(self, timeout: int):
52
+ self.timeout = timeout
53
+
54
+ def set_enable_tqdm(self, enable: bool):
55
+ self.enable_tqdm_bar = enable
56
+
57
+
58
+ config = Config()
iduedu/_version.py ADDED
@@ -0,0 +1 @@
1
+ VERSION = "0.1.0"
File without changes
@@ -0,0 +1,73 @@
1
+ from enum import Enum
2
+ from typing import Literal
3
+
4
+
5
+ class RegistrationStatus(Enum):
6
+ """
7
+ Enum for registration status of the road.
8
+ """
9
+
10
+ FEDERAL = 1
11
+ REGIONAL = 2
12
+ LOCAL = 3
13
+
14
+
15
+ class HighwayType(Enum):
16
+ """
17
+ Enum of highway types. Properties contain registration status & max speeds
18
+ """
19
+
20
+ MOTORWAY = "motorway"
21
+ TRUNK = "trunk"
22
+ PRIMARY = "primary"
23
+ SECONDARY = "secondary"
24
+ TERTIARY = "tertiary"
25
+ UNCLASSIFIED = "unclassified"
26
+ RESIDENTIAL = "residential"
27
+ MOTORWAY_LINK = "motorway_link"
28
+ TRUNK_LINK = "trunk_link"
29
+ PRIMARY_LINK = "primary_link"
30
+ SECONDARY_LINK = "secondary_link"
31
+ TERTIARY_LINK = "tertiary_link"
32
+ LIVING_STREET = "living_street"
33
+
34
+ @property
35
+ def reg_status(self) -> Literal[1, 2, 3]:
36
+ reg_status = {
37
+ HighwayType.MOTORWAY: RegistrationStatus.FEDERAL,
38
+ HighwayType.TRUNK: RegistrationStatus.FEDERAL,
39
+ HighwayType.PRIMARY: RegistrationStatus.REGIONAL,
40
+ HighwayType.SECONDARY: RegistrationStatus.REGIONAL,
41
+ HighwayType.TERTIARY: RegistrationStatus.LOCAL,
42
+ HighwayType.UNCLASSIFIED: RegistrationStatus.LOCAL,
43
+ HighwayType.RESIDENTIAL: RegistrationStatus.LOCAL,
44
+ HighwayType.MOTORWAY_LINK: RegistrationStatus.FEDERAL,
45
+ HighwayType.TRUNK_LINK: RegistrationStatus.FEDERAL,
46
+ HighwayType.PRIMARY_LINK: RegistrationStatus.REGIONAL,
47
+ HighwayType.SECONDARY_LINK: RegistrationStatus.REGIONAL,
48
+ HighwayType.TERTIARY_LINK: RegistrationStatus.LOCAL,
49
+ HighwayType.LIVING_STREET: RegistrationStatus.LOCAL,
50
+ }
51
+ return reg_status[self].value
52
+
53
+ @property
54
+ def max_speed(self) -> float:
55
+ """
56
+ Average speed in m/min.
57
+ """
58
+ speeds = { # km/h
59
+ HighwayType.MOTORWAY: 110,
60
+ HighwayType.TRUNK: 90,
61
+ HighwayType.PRIMARY: 60,
62
+ HighwayType.SECONDARY: 60,
63
+ HighwayType.TERTIARY: 60,
64
+ HighwayType.UNCLASSIFIED: 40,
65
+ HighwayType.RESIDENTIAL: 40,
66
+ HighwayType.MOTORWAY_LINK: 90,
67
+ HighwayType.TRUNK_LINK: 90,
68
+ HighwayType.PRIMARY_LINK: 60,
69
+ HighwayType.SECONDARY_LINK: 60,
70
+ HighwayType.TERTIARY_LINK: 60,
71
+ HighwayType.LIVING_STREET: 20,
72
+ }
73
+ return speeds[self] * 1000 / 60 # metres per minute
@@ -0,0 +1,25 @@
1
+ from enum import Enum
2
+
3
+
4
+ class PublicTrasport(Enum):
5
+ """
6
+ Enumeration class for edge types in graphs.
7
+ """
8
+
9
+ SUBWAY = "subway"
10
+ BUS = "bus"
11
+ TRAM = "tram"
12
+ TROLLEYBUS = "trolleybus"
13
+
14
+ @property
15
+ def avg_speed(self) -> float:
16
+ """
17
+ Average speed in m/min.
18
+ """
19
+ speeds = { # km/h
20
+ PublicTrasport.SUBWAY: 40,
21
+ PublicTrasport.BUS: 20,
22
+ PublicTrasport.TRAM: 20,
23
+ PublicTrasport.TROLLEYBUS: 18,
24
+ }
25
+ return speeds[self] * 1000 / 60
File without changes
@@ -0,0 +1,126 @@
1
+ import geopandas as gpd
2
+ import osm2geojson
3
+ import osmnx as ox
4
+ import pandas as pd
5
+ import requests
6
+ from shapely import MultiPolygon, Polygon, unary_union
7
+
8
+ from iduedu._config import config
9
+
10
+ logger = config.logger
11
+
12
+
13
+ class RequestError(RuntimeError):
14
+ def __init__(self, message, status_code=None, reason=None, response_text=None, response_content=None):
15
+ super().__init__(message)
16
+ self.status_code = status_code
17
+ self.reason = reason
18
+ self.response_text = response_text
19
+ self.response_content = response_content
20
+
21
+ def __str__(self):
22
+ return f"{super().__str__()} (status: {self.status_code}, reason: {self.reason})"
23
+
24
+
25
+ def get_boundary_by_osm_id(osm_id) -> MultiPolygon | Polygon:
26
+ overpass_query = f"""
27
+ [out:json];
28
+ (
29
+ relation({osm_id});
30
+ );
31
+ out geom;
32
+ """
33
+ logger.debug(f"Downloading territory bounds with osm_id <{osm_id}> ...")
34
+ result = requests.get(config.overpass_url, params={"data": overpass_query}, timeout=config.timeout)
35
+ if result.status_code == 200:
36
+ json_result = result.json()
37
+ boundary = osm2geojson.json2geojson(json_result)
38
+ boundary = gpd.GeoDataFrame.from_features(boundary["features"]).set_crs(4326)
39
+ poly = unary_union(boundary.geometry)
40
+ return poly
41
+ raise RequestError(
42
+ message=f"Request failed with status code {result.status_code}, reason: {result.reason}",
43
+ status_code=result.status_code,
44
+ reason=result.reason,
45
+ response_text=result.text,
46
+ response_content=result.content,
47
+ )
48
+
49
+
50
+ def get_boundary_by_name(territory_name: str) -> Polygon | MultiPolygon:
51
+ logger.debug(f"Downloading territory bounds with name <{territory_name}> ...")
52
+ place = ox.geocode_to_gdf(territory_name)
53
+ return unary_union(place.geometry)
54
+
55
+
56
+ def get_boundary(
57
+ osm_id: int | None = None, territory_name: str | None = None, polygon: Polygon | MultiPolygon | None = None
58
+ ) -> Polygon:
59
+ """
60
+ Retrieve the boundary polygon for a given territory, either by OSM ID, territory name, or an existing polygon.
61
+ If a MultiPolygon is provided, it will be converted to a convex hull.
62
+
63
+ Parameters
64
+ ----------
65
+ osm_id : int, optional
66
+ OpenStreetMap ID of the territory to retrieve the boundary for.
67
+ Either this or `territory_name` must be provided.
68
+ territory_name : str, optional
69
+ Name of the territory to retrieve the boundary for. Either this or `osm_id` must be provided.
70
+ polygon : Polygon | MultiPolygon, optional
71
+ A custom polygon or MultiPolygon to use instead of querying by `osm_id` or `territory_name`.
72
+
73
+ Returns
74
+ -------
75
+ Polygon
76
+ The boundary polygon for the specified territory.
77
+ If a MultiPolygon was provided, it will return the convex hull.
78
+
79
+ Raises
80
+ ------
81
+ ValueError
82
+ If neither `osm_id`, `territory_name`, nor `polygon` are provided.
83
+
84
+ Examples
85
+ --------
86
+ >>> boundary = get_boundary(osm_id=123456)
87
+ >>> boundary = get_boundary(territory_name="New York")
88
+ >>> boundary = get_boundary(polygon=some_polygon)
89
+ """
90
+
91
+ if osm_id is None and territory_name is None and polygon is None:
92
+ raise ValueError("Either osm_id or name or polygon must be specified")
93
+ if osm_id:
94
+ polygon: Polygon = get_boundary_by_osm_id(osm_id)
95
+ elif territory_name:
96
+ polygon: Polygon = get_boundary_by_name(territory_name)
97
+
98
+ if isinstance(polygon, MultiPolygon):
99
+ polygon: Polygon = polygon.convex_hull
100
+
101
+ return polygon
102
+
103
+
104
+ def get_routes_by_poly(polygon: Polygon, public_transport_type: str) -> pd.DataFrame:
105
+ polygon_coords = " ".join(f"{y} {x}" for x, y in polygon.exterior.coords[:-1])
106
+ overpass_query = f"""
107
+ [out:json];
108
+ (
109
+ relation(poly:"{polygon_coords}")['route'='{public_transport_type}'];
110
+ );
111
+ out geom;
112
+ """
113
+ logger.debug(f"Downloading routes from OSM with type <{public_transport_type}> ...")
114
+ result = requests.post(config.overpass_url, data={"data": overpass_query}, timeout=config.timeout)
115
+ if result.status_code == 200:
116
+ json_result = result.json()["elements"]
117
+ data = pd.DataFrame(json_result)
118
+ data["transport_type"] = public_transport_type
119
+ return data
120
+ raise RequestError(
121
+ message=f"Request failed with status code {result.status_code}, reason: {result.reason}",
122
+ status_code=result.status_code,
123
+ reason=result.reason,
124
+ response_text=result.text,
125
+ response_content=result.content,
126
+ )
@@ -0,0 +1,247 @@
1
+ import re
2
+
3
+ import geopandas as gpd
4
+ import networkx as nx
5
+ import osmnx as ox
6
+ import pandas as pd
7
+ from shapely import MultiPolygon, Polygon, unary_union
8
+ from tqdm.auto import tqdm
9
+
10
+ from iduedu import config
11
+ from iduedu.enums.drive_enums import HighwayType
12
+ from iduedu.modules.downloaders import get_boundary
13
+ from iduedu.utils.utils import estimate_crs_for_bounds
14
+
15
+ logger = config.logger
16
+
17
+ BASE_FILTER = "['highway'~'" + "|".join([h.value for h in HighwayType]) + "']"
18
+
19
+
20
+ def highway_type_to_reg(highway_type) -> int:
21
+ """
22
+ Determine the reg_status based on highway type.
23
+ """
24
+ try:
25
+ if isinstance(highway_type, list):
26
+ reg_values = [HighwayType[ht.upper()].reg_status for ht in highway_type]
27
+ return min(reg_values)
28
+ return HighwayType[highway_type.upper()].reg_status
29
+ except KeyError:
30
+ return 3
31
+
32
+
33
+ def determine_reg(name_roads, highway_type=None) -> int:
34
+ """
35
+ Determine the reg_status based on road_name.
36
+ """
37
+
38
+ if isinstance(name_roads, list):
39
+ for item in name_roads:
40
+ if re.match(r"^[МАР]", str(item)):
41
+ return 1
42
+ if re.match(r"^\d.*[A-Za-zА-Яа-я]", str(item)):
43
+ return 2
44
+ return 3
45
+ if pd.isna(name_roads):
46
+ # Выставление значения по типу дороги, если значение NaN
47
+ if highway_type:
48
+ return highway_type_to_reg(highway_type)
49
+ return 3
50
+ if re.match(r"^[МАР]", str(name_roads)):
51
+ return 1
52
+ if re.match(r"^\d.*[A-Za-zА-Яа-я]", str(name_roads)):
53
+ return 2
54
+ return 3
55
+
56
+
57
+ def get_max_speed(highway_types) -> float:
58
+ """
59
+ Determine the speed based on road_name.
60
+ """
61
+ # Проверяем, является ли highway_types списком.
62
+ try:
63
+ if isinstance(highway_types, list):
64
+ max_speeds = []
65
+ for ht in highway_types:
66
+ try:
67
+ highway_enum = HighwayType[ht.upper()]
68
+ max_speeds.append(highway_enum.max_speed)
69
+ except KeyError:
70
+ logger.debug(f"{ht} not found in HighwayType enum, skipping.")
71
+ if max_speeds:
72
+ return max(max_speeds)
73
+ else:
74
+ logger.debug("No valid highway types provided, returning 40 km/h.")
75
+ return 40 * 1000 / 60
76
+ return HighwayType[highway_types.upper()].max_speed
77
+ except KeyError:
78
+ return 40 * 1000 / 60
79
+
80
+
81
+ def get_drive_graph_by_poly(
82
+ polygon: Polygon | MultiPolygon, additional_edgedata=None, road_filter: str = None
83
+ ) -> nx.MultiDiGraph:
84
+ if additional_edgedata is None:
85
+ additional_edgedata = []
86
+ if not road_filter:
87
+ road_filter = BASE_FILTER
88
+ if isinstance(polygon, MultiPolygon):
89
+ polygon = unary_union(polygon)
90
+ if isinstance(polygon, MultiPolygon):
91
+ polygon = polygon.convex_hull
92
+
93
+ graph = ox.graph_from_polygon(
94
+ polygon,
95
+ network_type="drive",
96
+ custom_filter=road_filter,
97
+ truncate_by_edge=False,
98
+ )
99
+ local_crs = estimate_crs_for_bounds(*polygon.bounds).to_epsg()
100
+
101
+ nodes, edges = ox.graph_to_gdfs(graph)
102
+ edges: gpd.GeoDataFrame
103
+ edges.reset_index(inplace=True)
104
+ if "ref" not in edges.columns:
105
+ edges["ref"] = pd.NA
106
+ edges["reg"] = edges.apply(lambda row: determine_reg(row["ref"], row["highway"]), axis=1)
107
+
108
+ nodes.to_crs(local_crs, inplace=True)
109
+ nodes[["x", "y"]] = nodes.apply(lambda row: (row.geometry.x, row.geometry.y), axis=1, result_type="expand")
110
+ edges.to_crs(local_crs, inplace=True)
111
+
112
+ edges["maxspeed"] = edges["highway"].apply(get_max_speed)
113
+
114
+ edges[["length_meter", "time_min"]] = edges.apply(
115
+ lambda row: (round(row.geometry.length, 3), round(row.geometry.length / row.maxspeed, 3)),
116
+ axis=1,
117
+ result_type="expand",
118
+ )
119
+ edgesdata = ["u", "v", "key", "length_meter", "time_min", "geometry"] + additional_edgedata
120
+
121
+ edges = edges[edgesdata]
122
+
123
+ edges.set_index(["u", "v", "key"], inplace=True)
124
+ graph = ox.graph_from_gdfs(nodes, edges)
125
+ graph.graph["crs"] = local_crs
126
+ logger.debug('Done!')
127
+ return graph
128
+
129
+
130
+ def get_drive_graph(
131
+ osm_id: int | None = None,
132
+ territory_name: str | None = None,
133
+ polygon: Polygon | MultiPolygon | None = None,
134
+ additional_edgedata=None,
135
+ ):
136
+ """
137
+ Generate a road network graph for driving within the specified territory or polygon.
138
+ Optionally, include additional edge data such as highway type, max speed, registration status and road name.
139
+
140
+ Parameters
141
+ ----------
142
+ osm_id : int, optional
143
+ OpenStreetMap ID of the territory to build the graph for. Either this or `territory_name` must be provided.
144
+ territory_name : str, optional
145
+ Name of the territory to build the graph for. Either this or `osm_id` must be provided.
146
+ polygon : Polygon | MultiPolygon, optional
147
+ A custom polygon or MultiPolygon to define the area for the road network. Must be in CRS 4326.
148
+ additional_edgedata : list[str], optional
149
+ List of additional edge data attributes to include in the graph. Possible values include
150
+ ['highway', 'maxspeed', 'reg', 'ref', 'name'] or any other, that exist in OSM. Defaults to None.
151
+
152
+ Returns
153
+ -------
154
+ networkx.Graph
155
+ A road network graph for the specified territory or polygon, with optional additional edge data.
156
+
157
+ Examples
158
+ --------
159
+ >>> drive_graph = get_drive_graph(osm_id=1114252)
160
+ >>> drive_graph = get_drive_graph(territory_name="Санкт-Петербург", additional_edgedata=['highway', 'maxspeed'])
161
+ >>> drive_graph = get_drive_graph(polygon=some_polygon, additional_edgedata=['name', 'ref'])
162
+
163
+ Notes
164
+ -----
165
+ Road speeds are defined in `iduedu.enums.drive_enums.py`.
166
+ The CRS for the graph is estimated based on the bounds of the provided/downloaded polygon, stored in G.graph['crs'].
167
+ """
168
+
169
+ polygon = get_boundary(osm_id, territory_name, polygon)
170
+
171
+ return get_drive_graph_by_poly(polygon, additional_edgedata=additional_edgedata)
172
+
173
+
174
+ def get_walk_graph(
175
+ osm_id: int | None = None,
176
+ territory_name: str | None = None,
177
+ polygon: Polygon | MultiPolygon | None = None,
178
+ walk_speed: float = 5 * 1000 / 60,
179
+ ):
180
+ """
181
+ Generate a pedestrian road network graph within the specified territory or polygon.
182
+ The graph's edges includes calculated walking times based on the specified walking speed.
183
+
184
+ Parameters
185
+ ----------
186
+ osm_id : int, optional
187
+ OpenStreetMap ID of the territory to build the walking graph for.
188
+ Either this or `territory_name` must be provided.
189
+ territory_name : str, optional
190
+ Name of the territory to build the walking graph for. Either this or `osm_id` must be provided.
191
+ polygon : Polygon | MultiPolygon, optional
192
+ A custom polygon or MultiPolygon to define the area for the pedestrian network. Must be in CRS 4326.
193
+ walk_speed : float, optional
194
+ Walking speed in meters per minute. Defaults to 5 km/h (approximately 83.33 meters per minute).
195
+
196
+ Returns
197
+ -------
198
+ networkx.Graph
199
+ A pedestrian road network graph with edge lengths and walking times for the specified territory or polygon.
200
+
201
+ Examples
202
+ --------
203
+ >>> walk_graph = get_walk_graph(osm_id=1114252)
204
+ >>> walk_graph = get_walk_graph(territory_name="Санкт-Петербург", walk_speed=5)
205
+ >>> walk_graph = get_walk_graph(polygon=some_polygon)
206
+
207
+ Notes
208
+ -----
209
+ The CRS for the graph is estimated based on the bounds of the provided/downloaded polygon, stored in G.graph['crs'].
210
+ """
211
+
212
+ polygon = get_boundary(osm_id, territory_name, polygon)
213
+
214
+ logger.debug("Downloading walk graph from OSM ...")
215
+ graph = ox.graph_from_polygon(polygon, network_type="walk", truncate_by_edge=False, simplify=True)
216
+ local_crs = estimate_crs_for_bounds(*polygon.bounds).to_epsg()
217
+
218
+ nodes, edges = ox.graph_to_gdfs(graph)
219
+ nodes.to_crs(local_crs, inplace=True)
220
+ nodes[["x", "y"]] = nodes.apply(lambda row: (row.geometry.x, row.geometry.y), axis=1, result_type="expand")
221
+ nodes = nodes[["x", "y"]]
222
+ edges.reset_index(inplace=True)
223
+ edges.to_crs(local_crs, inplace=True)
224
+ tqdm.pandas(desc="Calculating the weights of the graph ...", disable=not config.enable_tqdm_bar)
225
+ edges[["length_meter", "time_min"]] = edges.progress_apply(
226
+ lambda row: (round(row.geometry.length, 3), round(row.geometry.length / walk_speed, 3)),
227
+ axis=1,
228
+ result_type="expand",
229
+ )
230
+ edges["type"] = "walk"
231
+ edges = edges[
232
+ [
233
+ "u",
234
+ "v",
235
+ "key",
236
+ "length_meter",
237
+ "time_min",
238
+ "type",
239
+ "geometry",
240
+ ]
241
+ ]
242
+ edges.set_index(["u", "v", "key"], inplace=True)
243
+ graph = ox.graph_from_gdfs(nodes, edges)
244
+ graph.graph["crs"] = local_crs
245
+ graph.graph["walk_speed"] = walk_speed
246
+ logger.debug('Done!')
247
+ return graph
@@ -0,0 +1,86 @@
1
+ import concurrent.futures
2
+
3
+ import networkx as nx
4
+ from shapely import MultiPolygon, Polygon
5
+
6
+ from iduedu import config
7
+ from iduedu.modules.downloaders import get_boundary
8
+ from iduedu.modules.drive_walk_builder import get_walk_graph
9
+ from iduedu.modules.pt_walk_joiner import join_pt_walk_graph
10
+ from iduedu.modules.public_transport_builder import get_all_public_transport_graph
11
+
12
+ logger = config.logger
13
+
14
+
15
+ def get_intermodal_graph(
16
+ osm_id: int | None = None,
17
+ territory_name: str | None = None,
18
+ polygon: Polygon | MultiPolygon | None = None,
19
+ clip_by_bounds: bool = False,
20
+ keep_routes_geom: bool = True,
21
+ ) -> nx.Graph:
22
+ """
23
+ Generate an intermodal transport graph that combines public transport and pedestrian networks,
24
+ with platforms serving as connection points between the two graphs.
25
+
26
+ Parameters
27
+ ----------
28
+ osm_id : int, optional
29
+ OpenStreetMap ID of the territory. Either this or `territory_name` must be provided.
30
+ territory_name : str, optional
31
+ Name of the territory to generate the intermodal transport network for.
32
+ Either this or `osm_id` must be provided.
33
+ polygon : Polygon | MultiPolygon, optional
34
+ A custom polygon or MultiPolygon defining the area for the intermodal network. Must be in CRS 4326.
35
+ clip_by_bounds : bool, optional
36
+ If True, clips the public transport network to the bounds of the provided polygon. Defaults to False.
37
+ keep_routes_geom : bool, optional
38
+
39
+ Returns
40
+ -------
41
+ nx.Graph
42
+ An intermodal network graph combining public transport and pedestrian routes, where public transport platforms
43
+ are linked to nearby walking routes.
44
+
45
+ Raises
46
+ ------
47
+ ValueError
48
+ If no valid `osm_id`, `territory_name`, or `polygon` is provided.
49
+
50
+ Warnings
51
+ --------
52
+ Logs a warning if the public transport graph is empty and only returns the pedestrian graph.
53
+
54
+ Examples
55
+ --------
56
+ >>> intermodal_graph = get_intermodal_graph(osm_id=1114252, clip_by_bounds=True)
57
+ >>> intermodal_graph = get_intermodal_graph(territory_name="Санкт-Петербург", polygon=some_polygon)
58
+
59
+ Notes
60
+ -----
61
+ The function concurrently downloads and processes both the pedestrian and public transport graphs,
62
+ then combines them using platforms as connection points.
63
+ If the public transport graph is empty, only the pedestrian graph is returned.
64
+ The CRS for the graph is estimated based on the bounds of the provided/downloaded polygon, stored in G.graph['crs'].
65
+ """
66
+
67
+ boundary = get_boundary(osm_id, territory_name, polygon)
68
+ with concurrent.futures.ThreadPoolExecutor() as executor:
69
+ walk_graph_future = executor.submit(get_walk_graph, polygon=boundary)
70
+ logger.debug("Started downloading and parsing walk graph...")
71
+ pt_graph_future = executor.submit(
72
+ get_all_public_transport_graph,
73
+ polygon=boundary,
74
+ clip_by_bounds=clip_by_bounds,
75
+ keep_geometry=keep_routes_geom,
76
+ )
77
+ logger.debug("Started downloading and parsing public trasport graph...")
78
+ pt_g = pt_graph_future.result()
79
+ logger.debug("Public trasport graph done!")
80
+ walk_g = walk_graph_future.result()
81
+ logger.debug("Walk graph done!")
82
+ if len(pt_g.nodes()) == 0:
83
+ logger.warning("Public trasport graph is empty! Returning only walk graph.")
84
+ return walk_g
85
+ intermodal = join_pt_walk_graph(pt_g, walk_g)
86
+ return intermodal