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 +11 -0
- iduedu/_api.py +7 -0
- iduedu/_config.py +58 -0
- iduedu/_version.py +1 -0
- iduedu/enums/__init__.py +0 -0
- iduedu/enums/drive_enums.py +73 -0
- iduedu/enums/pt_enums.py +25 -0
- iduedu/modules/__init__.py +0 -0
- iduedu/modules/downloaders.py +126 -0
- iduedu/modules/drive_walk_builder.py +247 -0
- iduedu/modules/intermodal_builder.py +86 -0
- iduedu/modules/matrix_builder.py +104 -0
- iduedu/modules/pt_walk_joiner.py +179 -0
- iduedu/modules/public_transport_builder.py +266 -0
- iduedu/modules/routes_parser.py +324 -0
- iduedu/utils/__init__.py +0 -0
- iduedu/utils/utils.py +36 -0
- iduedu-0.1.0.dist-info/METADATA +59 -0
- iduedu-0.1.0.dist-info/RECORD +20 -0
- iduedu-0.1.0.dist-info/WHEEL +4 -0
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"
|
iduedu/enums/__init__.py
ADDED
|
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
|
iduedu/enums/pt_enums.py
ADDED
|
@@ -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
|