guohanghui commited on
Commit
e4e2f45
·
verified ·
1 Parent(s): 26cb575

Upload 79 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. Dockerfile +18 -0
  2. README.md +26 -4
  3. app.py +45 -0
  4. osmnx/mcp_output/README_MCP.md +70 -0
  5. osmnx/mcp_output/analysis.json +251 -0
  6. osmnx/mcp_output/diff_report.md +63 -0
  7. osmnx/mcp_output/mcp_plugin/__init__.py +0 -0
  8. osmnx/mcp_output/mcp_plugin/adapter.py +242 -0
  9. osmnx/mcp_output/mcp_plugin/main.py +13 -0
  10. osmnx/mcp_output/mcp_plugin/mcp_service.py +79 -0
  11. osmnx/mcp_output/requirements.txt +11 -0
  12. osmnx/mcp_output/start_mcp.py +30 -0
  13. osmnx/mcp_output/workflow_summary.json +194 -0
  14. osmnx/source/.pre-commit-config.yaml +53 -0
  15. osmnx/source/CHANGELOG.md +690 -0
  16. osmnx/source/CITATION.cff +32 -0
  17. osmnx/source/CONTRIBUTING.md +39 -0
  18. osmnx/source/LICENSE.txt +21 -0
  19. osmnx/source/README.md +33 -0
  20. osmnx/source/__init__.py +4 -0
  21. osmnx/source/docs/.readthedocs.yaml +16 -0
  22. osmnx/source/docs/Makefile +20 -0
  23. osmnx/source/docs/make.bat +35 -0
  24. osmnx/source/docs/requirements-docs.txt +4 -0
  25. osmnx/source/docs/source/conf.py +64 -0
  26. osmnx/source/docs/source/further-reading.rst +40 -0
  27. osmnx/source/docs/source/getting-started.rst +184 -0
  28. osmnx/source/docs/source/index.rst +84 -0
  29. osmnx/source/docs/source/installation.rst +37 -0
  30. osmnx/source/docs/source/internals-reference.rst +196 -0
  31. osmnx/source/docs/source/user-reference.rst +142 -0
  32. osmnx/source/environments/create_conda_env.sh +23 -0
  33. osmnx/source/environments/docker/Dockerfile +24 -0
  34. osmnx/source/environments/docker/build_image.sh +7 -0
  35. osmnx/source/environments/docker/install.sh +21 -0
  36. osmnx/source/osmnx/__init__.py +37 -0
  37. osmnx/source/osmnx/_api_v1.py +53 -0
  38. osmnx/source/osmnx/_errors.py +21 -0
  39. osmnx/source/osmnx/_http.py +332 -0
  40. osmnx/source/osmnx/_nominatim.py +151 -0
  41. osmnx/source/osmnx/_osm_xml.py +439 -0
  42. osmnx/source/osmnx/_overpass.py +493 -0
  43. osmnx/source/osmnx/_validate.py +387 -0
  44. osmnx/source/osmnx/bearing.py +302 -0
  45. osmnx/source/osmnx/convert.py +564 -0
  46. osmnx/source/osmnx/distance.py +545 -0
  47. osmnx/source/osmnx/elevation.py +331 -0
  48. osmnx/source/osmnx/features.py +734 -0
  49. osmnx/source/osmnx/geocoder.py +244 -0
  50. osmnx/source/osmnx/graph.py +863 -0
Dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10
2
+
3
+ RUN useradd -m -u 1000 user && python -m pip install --upgrade pip
4
+ USER user
5
+ ENV PATH="/home/user/.local/bin:$PATH"
6
+
7
+ WORKDIR /app
8
+
9
+ COPY --chown=user ./requirements.txt requirements.txt
10
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
11
+
12
+ COPY --chown=user . /app
13
+ ENV MCP_TRANSPORT=http
14
+ ENV MCP_PORT=7860
15
+
16
+ EXPOSE 7860
17
+
18
+ CMD ["python", "osmnx/mcp_output/start_mcp.py"]
README.md CHANGED
@@ -1,10 +1,32 @@
1
  ---
2
- title: Osmnx
3
- emoji: 💻
4
- colorFrom: indigo
5
  colorTo: purple
6
  sdk: docker
 
 
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Osmnx MCP
3
+ emoji: 🤖
4
+ colorFrom: blue
5
  colorTo: purple
6
  sdk: docker
7
+ sdk_version: "4.26.0"
8
+ app_file: app.py
9
  pinned: false
10
  ---
11
 
12
+ # Osmnx MCP Service
13
+
14
+ Auto-generated MCP service for osmnx.
15
+
16
+ ## Usage
17
+
18
+ ```
19
+ https://None-osmnx-mcp.hf.space/mcp
20
+ ```
21
+
22
+ ## Connect with Cursor
23
+
24
+ ```json
25
+ {
26
+ "mcpServers": {
27
+ "osmnx": {
28
+ "url": "https://None-osmnx-mcp.hf.space/mcp"
29
+ }
30
+ }
31
+ }
32
+ ```
app.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ import os
3
+ import sys
4
+
5
+ mcp_plugin_path = os.path.join(os.path.dirname(__file__), "osmnx", "mcp_output", "mcp_plugin")
6
+ sys.path.insert(0, mcp_plugin_path)
7
+
8
+ app = FastAPI(
9
+ title="Osmnx MCP Service",
10
+ description="Auto-generated MCP service for osmnx",
11
+ version="1.0.0"
12
+ )
13
+
14
+ @app.get("/")
15
+ def root():
16
+ return {
17
+ "service": "Osmnx MCP Service",
18
+ "version": "1.0.0",
19
+ "status": "running",
20
+ "transport": os.environ.get("MCP_TRANSPORT", "http")
21
+ }
22
+
23
+ @app.get("/health")
24
+ def health_check():
25
+ return {"status": "healthy", "service": "osmnx MCP"}
26
+
27
+ @app.get("/tools")
28
+ def list_tools():
29
+ try:
30
+ from mcp_service import create_app
31
+ mcp_app = create_app()
32
+ tools = []
33
+ for tool_name, tool_func in mcp_app.tools.items():
34
+ tools.append({
35
+ "name": tool_name,
36
+ "description": tool_func.__doc__ or "No description available"
37
+ })
38
+ return {"tools": tools}
39
+ except Exception as e:
40
+ return {"error": f"Failed to load tools: {str(e)}"}
41
+
42
+ if __name__ == "__main__":
43
+ import uvicorn
44
+ port = int(os.environ.get("PORT", 7860))
45
+ uvicorn.run(app, host="0.0.0.0", port=port)
osmnx/mcp_output/README_MCP.md ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # OSMnx MCP (Model Context Protocol) Service
2
+
3
+ ## Project Introduction
4
+
5
+ OSMnx is a Python package designed for downloading, modeling, analyzing, and visualizing street networks and other geospatial features from OpenStreetMap (OSM). It provides a simple interface to retrieve complex urban data and convert it into network graphs and other usable formats for spatial analysis. Key functionalities include graph creation, network analysis, routing, and visualization.
6
+
7
+ ## Installation Method
8
+
9
+ To install OSMnx, ensure you have Python installed, then use pip to install the package along with its dependencies:
10
+
11
+ - Required dependencies: `networkx`, `matplotlib`, `geopandas`, `shapely`, `requests`
12
+ - Optional dependency: `folium`
13
+
14
+ Install OSMnx via pip:
15
+
16
+ ```
17
+ pip install osmnx
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ Here's a quick example to get you started with OSMnx:
23
+
24
+ 1. **Create a graph from a place name:**
25
+
26
+ ```python
27
+ import osmnx as ox
28
+ G = ox.graph_from_place('Piedmont, California, USA', network_type='drive')
29
+ ```
30
+
31
+ 2. **Plot the graph:**
32
+
33
+ ```python
34
+ ox.plot_graph(G)
35
+ ```
36
+
37
+ 3. **Calculate distances:**
38
+
39
+ ```python
40
+ distance = ox.distance.great_circle_vec(lat1, lon1, lat2, lon2)
41
+ ```
42
+
43
+ ## Available Tools and Endpoints List
44
+
45
+ - **Graph Creation:**
46
+ - `graph_from_place()`: Create a graph from a place name.
47
+ - `graph_from_address()`: Create a graph from an address.
48
+ - `graph_from_point()`: Create a graph from a geographic point.
49
+
50
+ - **Visualization:**
51
+ - `plot_graph()`: Visualize the graph.
52
+ - `plot_footprints()`: Plot building footprints.
53
+
54
+ - **Distance Calculations:**
55
+ - `great_circle_vec()`: Calculate great-circle distance between points.
56
+ - `euclidean_dist_vec()`: Calculate Euclidean distance between points.
57
+
58
+ ## Common Issues and Notes
59
+
60
+ - **Dependencies:** Ensure all required dependencies are installed. Use the optional `folium` for interactive map visualizations.
61
+ - **Environment:** OSMnx is compatible with Python 3.6 and above. Ensure your environment is set up accordingly.
62
+ - **Performance:** Large datasets may require significant memory and processing power. Consider using a machine with adequate resources for large-scale analyses.
63
+
64
+ ## Reference Links or Documentation
65
+
66
+ - [OSMnx GitHub Repository](https://github.com/gboeing/osmnx)
67
+ - [OSMnx Documentation](https://osmnx.readthedocs.io/en/stable/)
68
+ - [OSMnx User Guide](https://osmnx.readthedocs.io/en/stable/user_guide.html)
69
+
70
+ For further details on the OSMnx MCP (Model Context Protocol) service, refer to the documentation and explore the various functionalities it offers for geospatial analysis and visualization.
osmnx/mcp_output/analysis.json ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "summary": {
3
+ "repository_url": "https://github.com/gboeing/osmnx",
4
+ "summary": "Imported via zip fallback, file count: 49",
5
+ "file_tree": {
6
+ ".github/ISSUE_TEMPLATE/bug_report.yml": {
7
+ "size": 4139
8
+ },
9
+ ".github/ISSUE_TEMPLATE/config.yml": {
10
+ "size": 28
11
+ },
12
+ ".github/ISSUE_TEMPLATE/feature_proposal.yml": {
13
+ "size": 2518
14
+ },
15
+ ".github/dependabot.yml": {
16
+ "size": 173
17
+ },
18
+ ".github/pull_request_template.md": {
19
+ "size": 946
20
+ },
21
+ ".github/workflows/build-publish-docker.yml": {
22
+ "size": 1519
23
+ },
24
+ ".github/workflows/build-publish-pypi.yml": {
25
+ "size": 1213
26
+ },
27
+ ".github/workflows/ci.yml": {
28
+ "size": 2027
29
+ },
30
+ ".github/workflows/test-docs-linkcheck.yml": {
31
+ "size": 1014
32
+ },
33
+ ".github/workflows/test-latest-deps.yml": {
34
+ "size": 1214
35
+ },
36
+ ".github/workflows/test-minimum-deps.yml": {
37
+ "size": 1319
38
+ },
39
+ ".pre-commit-config.yaml": {
40
+ "size": 1390
41
+ },
42
+ "CHANGELOG.md": {
43
+ "size": 31709
44
+ },
45
+ "CONTRIBUTING.md": {
46
+ "size": 3569
47
+ },
48
+ "LICENSE.txt": {
49
+ "size": 1109
50
+ },
51
+ "README.md": {
52
+ "size": 2558
53
+ },
54
+ "docs/.readthedocs.yaml": {
55
+ "size": 223
56
+ },
57
+ "docs/requirements-docs.txt": {
58
+ "size": 45
59
+ },
60
+ "docs/source/conf.py": {
61
+ "size": 1842
62
+ },
63
+ "osmnx/__init__.py": {
64
+ "size": 1189
65
+ },
66
+ "osmnx/_api_v1.py": {
67
+ "size": 2724
68
+ },
69
+ "osmnx/_errors.py": {
70
+ "size": 610
71
+ },
72
+ "osmnx/_http.py": {
73
+ "size": 11518
74
+ },
75
+ "osmnx/_nominatim.py": {
76
+ "size": 4900
77
+ },
78
+ "osmnx/_osm_xml.py": {
79
+ "size": 16252
80
+ },
81
+ "osmnx/_overpass.py": {
82
+ "size": 18059
83
+ },
84
+ "osmnx/_validate.py": {
85
+ "size": 13542
86
+ },
87
+ "osmnx/bearing.py": {
88
+ "size": 10769
89
+ },
90
+ "osmnx/convert.py": {
91
+ "size": 18490
92
+ },
93
+ "osmnx/distance.py": {
94
+ "size": 16700
95
+ },
96
+ "osmnx/elevation.py": {
97
+ "size": 11330
98
+ },
99
+ "osmnx/features.py": {
100
+ "size": 28647
101
+ },
102
+ "osmnx/geocoder.py": {
103
+ "size": 8631
104
+ },
105
+ "osmnx/graph.py": {
106
+ "size": 33070
107
+ },
108
+ "osmnx/io.py": {
109
+ "size": 15477
110
+ },
111
+ "osmnx/plot.py": {
112
+ "size": 33836
113
+ },
114
+ "osmnx/projection.py": {
115
+ "size": 6381
116
+ },
117
+ "osmnx/routing.py": {
118
+ "size": 21734
119
+ },
120
+ "osmnx/settings.py": {
121
+ "size": 8327
122
+ },
123
+ "osmnx/simplification.py": {
124
+ "size": 33518
125
+ },
126
+ "osmnx/stats.py": {
127
+ "size": 13492
128
+ },
129
+ "osmnx/truncate.py": {
130
+ "size": 6097
131
+ },
132
+ "osmnx/utils.py": {
133
+ "size": 6198
134
+ },
135
+ "osmnx/utils_geo.py": {
136
+ "size": 13934
137
+ },
138
+ "pyproject.toml": {
139
+ "size": 3463
140
+ },
141
+ "tests/.yamllint.yml": {
142
+ "size": 182
143
+ },
144
+ "tests/README.md": {
145
+ "size": 1703
146
+ },
147
+ "tests/test_osmnx.py": {
148
+ "size": 36044
149
+ },
150
+ "tests/verify_min_deps.py": {
151
+ "size": 1575
152
+ }
153
+ },
154
+ "processed_by": "zip_fallback",
155
+ "success": true
156
+ },
157
+ "structure": {
158
+ "packages": [
159
+ "source.osmnx"
160
+ ]
161
+ },
162
+ "dependencies": {
163
+ "has_environment_yml": false,
164
+ "has_requirements_txt": false,
165
+ "pyproject": true,
166
+ "setup_cfg": false,
167
+ "setup_py": false
168
+ },
169
+ "entry_points": {
170
+ "imports": [],
171
+ "cli": [],
172
+ "modules": []
173
+ },
174
+ "llm_analysis": {
175
+ "core_modules": [
176
+ {
177
+ "package": "source.osmnx",
178
+ "module": "osmnx.graph",
179
+ "functions": [
180
+ "graph_from_place",
181
+ "graph_from_address",
182
+ "graph_from_point"
183
+ ],
184
+ "classes": [
185
+ "Graph"
186
+ ],
187
+ "description": "Functions and classes for creating and manipulating graphs from OpenStreetMap data."
188
+ },
189
+ {
190
+ "package": "source.osmnx",
191
+ "module": "osmnx.plot",
192
+ "functions": [
193
+ "plot_graph",
194
+ "plot_footprints"
195
+ ],
196
+ "classes": [],
197
+ "description": "Functions for visualizing graphs and geographic data."
198
+ },
199
+ {
200
+ "package": "source.osmnx",
201
+ "module": "osmnx.distance",
202
+ "functions": [
203
+ "great_circle_vec",
204
+ "euclidean_dist_vec"
205
+ ],
206
+ "classes": [],
207
+ "description": "Functions for calculating distances between geographic points."
208
+ }
209
+ ],
210
+ "cli_commands": [],
211
+ "import_strategy": {
212
+ "primary": "import",
213
+ "fallback": "blackbox",
214
+ "confidence": 0.9
215
+ },
216
+ "dependencies": {
217
+ "required": [
218
+ "networkx",
219
+ "matplotlib",
220
+ "geopandas",
221
+ "shapely",
222
+ "requests"
223
+ ],
224
+ "optional": [
225
+ "folium"
226
+ ]
227
+ },
228
+ "risk_assessment": {
229
+ "import_feasibility": 0.9,
230
+ "intrusiveness_risk": "low",
231
+ "complexity": "medium"
232
+ }
233
+ },
234
+ "deepwiki_analysis": {
235
+ "repo_url": "https://github.com/gboeing/osmnx",
236
+ "repo_name": "osmnx",
237
+ "content": "gboeing/osmnx\nInstallation and Setup\nGetting Started\nArchitecture and Data Flow\nData Acquisition\nConfiguration and Settings\nCore Functionality\nGraph Creation and Manipulation\nNetwork Analysis\nRouting and Path Finding\nVisualization\nAdvanced Features\nElevation Data\nProjection and Coordinate Systems\nData Import and Export\nDevelopment and Contributing\nTesting and Quality Assurance\nCI/CD Pipeline\nDocker Environment\nVersion History and Roadmap\nCHANGELOG.md\nLICENSE.txt\ndocs/source/conf.py\ndocs/source/index.rst\ndocs/source/internals-reference.rst\ndocs/source/user-reference.rst\nosmnx/__init__.py\nosmnx/_version.py\nOSMnx is a Python package for downloading, modeling, analyzing, and visualizing street networks and other geospatial features from OpenStreetMap (OSM). It provides a simple interface to retrieve complex urban data and convert it into network graphs and other usable formats for spatial analysis. This document introduces the OSMnx system architecture and its core functionality. For installation instructions, seeInstallation and Setup, and for a quick start guide, seeGetting Started.\nPurpose and Scope\nOSMnx enables users to:\nDownload and model walking, driving, or biking networks with a single line of code\nWork with urban amenities/points of interest, building footprints, and transit infrastructure\nAnalyze network metrics and spatial characteristics\nIncorporate elevation data for 3D network analysis and grade calculations\nCalculate and visualize street orientation patterns and entropy\nPerform routing and trip planning with customizable impedance values\nProject and visualize networks and analysis results\nSources:README.md10-12docs/source/index.rst4-5\nSystem Architecture\nCore Components of OSMnx\nOSMnx SystemUser ApplicationOSMnx APIData Acquisition LayerProcessing LayerAnalysis LayerVisualization LayerI/O LayerOverpass API Client(_overpass)Nominatim API Client(_nominatim)HTTP & Caching(_http)graph.py(Network Creation)projection.py(Coordinate Systems)simplification.py(Network Cleanup)convert.py(Data Conversion)distance.py(Distance Calculations)routing.py(Path Finding)stats.py(Network Metrics)bearing.py(Street Orientations)elevation.py(Elevation Data)plot.py(Map Visualization)io.py(Save/Load Graphs)_osm_xml.py(XML Handling)settings.py(Configuration)utils.py(Utilities)OpenStreetMapData\nOSMnx System\nUser Application\nData Acquisition Layer\nProcessing Layer\nAnalysis Layer\nVisualization Layer\nOverpass API Client(_overpass)\nNominatim API Client(_nominatim)\nHTTP & Caching(_http)\ngraph.py(Network Creation)\nprojection.py(Coordinate Systems)\nsimplification.py(Network Cleanup)\nconvert.py(Data Conversion)\ndistance.py(Distance Calculations)\nrouting.py(Path Finding)\nstats.py(Network Metrics)\nbearing.py(Street Orientations)\nelevation.py(Elevation Data)\nplot.py(Map Visualization)\nio.py(Save/Load Graphs)\n_osm_xml.py(XML Handling)\nsettings.py(Configuration)\nutils.py(Utilities)\nOpenStreetMapData\nSources:osmnx/__init__.py10-53docs/source/user-reference.rst\nOutputAnalysisGraph ProcessingData AcquisitionInput MethodsUser InputDefine Query Typegraph_from_place()graph_from_address()graph_from_point()graph_from_bbox()graph_from_polygon()graph_from_xml()geocoder.py_nominatim.py_overpass.py_http.pyOpenStreetMapData_osm_xml.pyNetworkX MultiDiGraphproject_graph()simplify_graph()consolidate_intersections()Add Attributes(speed, travel time,elevation, bearings)Path Finding(shortest_path,k_shortest_paths)Network Analysis(basic_stats)plot_graph()plot_graph_route()save_graphml()save_graph_geopackage()save_graph_xml()\nGraph Processing\nData Acquisition\nInput Methods\nDefine Query Type\ngraph_from_place()\ngraph_from_address()\ngraph_from_point()\ngraph_from_bbox()\ngraph_from_polygon()\ngraph_from_xml()\ngeocoder.py\n_nominatim.py\n_overpass.py\nOpenStreetMapData\n_osm_xml.py\nNetworkX MultiDiGraph\nproject_graph()\nsimplify_graph()\nconsolidate_intersections()\nAdd Attributes(speed, travel time,elevation, bearings)\nPath Finding(shortest_path,k_shortest_paths)\nNetwork Analysis(basic_stats)\nplot_graph()plot_graph_route()\nsave_graphml()save_graph_geopackage()save_graph_xml()\nSources:osmnx/__init__.py10-53CHANGELOG.md\nCore Functionality\nGraph Creation\nOSMnx provides multiple functions for downloading and creating street network graphs:\ngraph_from_place()\ngraph_from_address()\ngraph_from_point()\ngraph_from_bbox()\ngraph_from_polygon()\ngraph_from_xml()\nSources:osmnx/__init__.py27-32\nNetwork Types\nOSMnx can retrieve different types of street networks depending on thenetwork_typeparameter:\nnetwork_type\nNetwork Typeswalk(Pedestrian)bike(Cycling)drive(Driving)drive_service(Driving + Service Roads)all(All Public Ways)all_public(All Public Ways)\nNetwork Types\nwalk(Pedestrian)\nbike(Cycling)\ndrive(Driving)\ndrive_service(Driving + Service Roads)\nall(All Public Ways)\nall_public(All Public Ways)\nSources:CHANGELOG.md79-81\nData Processing and Analysis\nAfter creating a graph, OSMnx provides tools for processing, enriching, and analyzing the network:\nproject_graph()\nsimplify_graph()\nconsolidate_intersections()\nadd_edge_lengths()\nadd_edge_speeds()\nadd_edge_travel_times()\nadd_edge_bearings()\nadd_edge_grades()\nadd_node_elevations_google()\nadd_node_elevations_raster()\nshortest_path()\nk_shortest_paths()\nbasic_stats()\nSources:osmnx/__init__.py10-53CHANGELOG.md\nVisualization\nOSMnx provides multiple plotting functions to visualize networks and analysis results:\nplot_graph()\nplot_graph_route()\nplot_graph_routes()\nplot_footprints()\nplot_figure_ground()\nplot_orientation()\nSources:osmnx/__init__.py37-42\nOSMnx can save and load graphs in various formats:\nsave_graphml()\nload_graphml()\nsave_graph_geopackage()\nsave_graph_xml()\nSources:osmnx/__init__.py33-36\nVersion and Development\nOSMnx is currently at version 2.0.2, released under the MIT license. The package is actively maintained with regular updates and improvements. For version history and roadmap information, seeVersion History and Roadmap.\nSources:osmnx/_version.py3LICENSE.txtCHANGELOG.md\nRefresh this wiki\nOn this page\nPurpose and Scope\nSystem Architecture\nCore Components of OSMnx\nCore Functionality\nGraph Creation\nNetwork Types\nData Processing and Analysis\nVisualization\nVersion and Development",
238
+ "model": "gpt-4o-2024-08-06",
239
+ "source": "selenium",
240
+ "success": true
241
+ },
242
+ "deepwiki_options": {
243
+ "enabled": true,
244
+ "model": "gpt-4o-2024-08-06"
245
+ },
246
+ "risk": {
247
+ "import_feasibility": 0.9,
248
+ "intrusiveness_risk": "low",
249
+ "complexity": "medium"
250
+ }
251
+ }
osmnx/mcp_output/diff_report.md ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # OSMnx Project Difference Report
2
+
3
+ **Date:** February 6, 2026
4
+ **Time:** 14:18:16
5
+ **Repository:** osmnx
6
+ **Project Type:** Python Library
7
+ **Intrusiveness:** None
8
+ **Workflow Status:** Success
9
+ **Test Status:** Failed
10
+
11
+ ## Project Overview
12
+
13
+ OSMnx is a Python library designed to facilitate the acquisition, construction, analysis, and visualization of street networks from OpenStreetMap. It provides tools for geospatial analysis and urban planning, making it a valuable resource for researchers and developers working with spatial data.
14
+
15
+ ## Difference Analysis
16
+
17
+ ### New Files
18
+
19
+ In this update, 8 new files have been added to the repository. These files likely introduce new features or enhancements to the existing functionality of the OSMnx library. However, no existing files were modified, indicating that the core functionality remains unchanged.
20
+
21
+ ### Modified Files
22
+
23
+ There were no modifications to existing files. This suggests that the new features or enhancements were implemented in a way that does not alter the current codebase, maintaining backward compatibility.
24
+
25
+ ## Technical Analysis
26
+
27
+ ### Workflow Status
28
+
29
+ The workflow status is marked as successful, indicating that the automated processes for building, testing, and deploying the project were executed without errors. This suggests that the integration of new files was handled smoothly.
30
+
31
+ ### Test Status
32
+
33
+ The test status is marked as failed, which is a critical issue. This failure indicates that the new additions may have introduced bugs or that the existing test suite does not adequately cover the new functionality. It is essential to address these test failures to ensure the reliability and stability of the library.
34
+
35
+ ## Recommendations and Improvements
36
+
37
+ 1. **Investigate Test Failures:** Conduct a thorough investigation into the test failures to identify the root causes. This may involve reviewing the new files for potential bugs or updating the test suite to cover new functionalities.
38
+
39
+ 2. **Enhance Test Coverage:** Ensure that the test suite is comprehensive and includes tests for all new features. This will help in maintaining the integrity of the library and prevent future test failures.
40
+
41
+ 3. **Documentation Update:** Update the project documentation to reflect the new features and enhancements. This will assist users in understanding and utilizing the new capabilities of the library.
42
+
43
+ 4. **Code Review:** Conduct a code review of the new files to ensure they adhere to the project's coding standards and best practices. This can help in identifying potential issues early.
44
+
45
+ ## Deployment Information
46
+
47
+ The deployment process appears to have been successful, as indicated by the workflow status. However, given the test failures, it is advisable to hold off on deploying the new version to production until the issues are resolved.
48
+
49
+ ## Future Planning
50
+
51
+ 1. **Bug Fixes:** Prioritize fixing the test failures and any identified bugs in the new files.
52
+
53
+ 2. **Feature Expansion:** Consider expanding the new features based on user feedback and requirements.
54
+
55
+ 3. **Community Engagement:** Engage with the community to gather feedback on the new features and identify areas for improvement.
56
+
57
+ 4. **Regular Updates:** Plan for regular updates to the library to incorporate new features, bug fixes, and improvements.
58
+
59
+ ## Conclusion
60
+
61
+ The recent update to the OSMnx project introduces new features through the addition of 8 new files. While the integration process was successful, the test failures highlight the need for immediate attention to ensure the library's reliability. By addressing these issues and enhancing test coverage, the project can continue to provide valuable tools for geospatial analysis and urban planning.
62
+
63
+ ---
osmnx/mcp_output/mcp_plugin/__init__.py ADDED
File without changes
osmnx/mcp_output/mcp_plugin/adapter.py ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+
4
+ # Path settings
5
+ source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source")
6
+ sys.path.insert(0, source_path)
7
+
8
+ # Import statements
9
+ try:
10
+ from osmnx import (
11
+ graph_from_place, graph_from_address, graph_from_point, graph_from_bbox,
12
+ graph_from_polygon, graph_from_xml, project_graph, simplify_graph,
13
+ consolidate_intersections, add_edge_lengths, add_edge_speeds,
14
+ add_edge_travel_times, add_edge_bearings, add_edge_grades,
15
+ add_node_elevations_google, add_node_elevations_raster, shortest_path,
16
+ k_shortest_paths, basic_stats, plot_graph, plot_graph_route,
17
+ plot_graph_routes, plot_footprints, plot_figure_ground, plot_orientation,
18
+ save_graphml, load_graphml, save_graph_geopackage, save_graph_xml
19
+ )
20
+ import_success = True
21
+ except ImportError as e:
22
+ import_success = False
23
+ import_error = str(e)
24
+
25
+ class Adapter:
26
+ """
27
+ Adapter class for interfacing with the OSMnx library.
28
+ Provides methods for graph creation, manipulation, analysis, and visualization.
29
+ """
30
+
31
+ def __init__(self):
32
+ self.mode = "import" if import_success else "fallback"
33
+
34
+ # -------------------- Graph Creation Methods --------------------
35
+
36
+ def create_graph_from_place(self, place_name, network_type='all'):
37
+ """
38
+ Create a graph from a place name.
39
+
40
+ :param place_name: Name of the place to create the graph from.
41
+ :param network_type: Type of network to create.
42
+ :return: Dictionary with status and graph object or error message.
43
+ """
44
+ try:
45
+ graph = graph_from_place(place_name, network_type=network_type)
46
+ return {"status": "success", "graph": graph}
47
+ except Exception as e:
48
+ return {"status": "error", "message": str(e)}
49
+
50
+ def create_graph_from_address(self, address, network_type='all'):
51
+ """
52
+ Create a graph from an address.
53
+
54
+ :param address: Address to create the graph from.
55
+ :param network_type: Type of network to create.
56
+ :return: Dictionary with status and graph object or error message.
57
+ """
58
+ try:
59
+ graph = graph_from_address(address, network_type=network_type)
60
+ return {"status": "success", "graph": graph}
61
+ except Exception as e:
62
+ return {"status": "error", "message": str(e)}
63
+
64
+ def create_graph_from_point(self, point, dist=1000, network_type='all'):
65
+ """
66
+ Create a graph from a geographical point.
67
+
68
+ :param point: Tuple of (latitude, longitude).
69
+ :param dist: Distance around the point to create the graph.
70
+ :param network_type: Type of network to create.
71
+ :return: Dictionary with status and graph object or error message.
72
+ """
73
+ try:
74
+ graph = graph_from_point(point, dist=dist, network_type=network_type)
75
+ return {"status": "success", "graph": graph}
76
+ except Exception as e:
77
+ return {"status": "error", "message": str(e)}
78
+
79
+ def create_graph_from_bbox(self, north, south, east, west, network_type='all'):
80
+ """
81
+ Create a graph from a bounding box.
82
+
83
+ :param north: Northern latitude.
84
+ :param south: Southern latitude.
85
+ :param east: Eastern longitude.
86
+ :param west: Western longitude.
87
+ :param network_type: Type of network to create.
88
+ :return: Dictionary with status and graph object or error message.
89
+ """
90
+ try:
91
+ graph = graph_from_bbox(north, south, east, west, network_type=network_type)
92
+ return {"status": "success", "graph": graph}
93
+ except Exception as e:
94
+ return {"status": "error", "message": str(e)}
95
+
96
+ def create_graph_from_polygon(self, polygon, network_type='all'):
97
+ """
98
+ Create a graph from a polygon.
99
+
100
+ :param polygon: Polygon geometry.
101
+ :param network_type: Type of network to create.
102
+ :return: Dictionary with status and graph object or error message.
103
+ """
104
+ try:
105
+ graph = graph_from_polygon(polygon, network_type=network_type)
106
+ return {"status": "success", "graph": graph}
107
+ except Exception as e:
108
+ return {"status": "error", "message": str(e)}
109
+
110
+ def create_graph_from_xml(self, filepath, network_type='all'):
111
+ """
112
+ Create a graph from an XML file.
113
+
114
+ :param filepath: Path to the XML file.
115
+ :param network_type: Type of network to create.
116
+ :return: Dictionary with status and graph object or error message.
117
+ """
118
+ try:
119
+ graph = graph_from_xml(filepath, network_type=network_type)
120
+ return {"status": "success", "graph": graph}
121
+ except Exception as e:
122
+ return {"status": "error", "message": str(e)}
123
+
124
+ # -------------------- Graph Manipulation Methods --------------------
125
+
126
+ def project_graph(self, graph):
127
+ """
128
+ Project a graph to UTM.
129
+
130
+ :param graph: Graph to project.
131
+ :return: Dictionary with status and projected graph or error message.
132
+ """
133
+ try:
134
+ projected_graph = project_graph(graph)
135
+ return {"status": "success", "projected_graph": projected_graph}
136
+ except Exception as e:
137
+ return {"status": "error", "message": str(e)}
138
+
139
+ def simplify_graph(self, graph):
140
+ """
141
+ Simplify a graph.
142
+
143
+ :param graph: Graph to simplify.
144
+ :return: Dictionary with status and simplified graph or error message.
145
+ """
146
+ try:
147
+ simplified_graph = simplify_graph(graph)
148
+ return {"status": "success", "simplified_graph": simplified_graph}
149
+ except Exception as e:
150
+ return {"status": "error", "message": str(e)}
151
+
152
+ def consolidate_intersections(self, graph, tolerance=10, rebuild_graph=True):
153
+ """
154
+ Consolidate intersections in a graph.
155
+
156
+ :param graph: Graph to process.
157
+ :param tolerance: Tolerance for consolidating intersections.
158
+ :param rebuild_graph: Whether to rebuild the graph after consolidation.
159
+ :return: Dictionary with status and processed graph or error message.
160
+ """
161
+ try:
162
+ consolidated_graph = consolidate_intersections(graph, tolerance=tolerance, rebuild_graph=rebuild_graph)
163
+ return {"status": "success", "consolidated_graph": consolidated_graph}
164
+ except Exception as e:
165
+ return {"status": "error", "message": str(e)}
166
+
167
+ # -------------------- Graph Analysis Methods --------------------
168
+
169
+ def calculate_basic_stats(self, graph):
170
+ """
171
+ Calculate basic statistics of a graph.
172
+
173
+ :param graph: Graph to analyze.
174
+ :return: Dictionary with status and statistics or error message.
175
+ """
176
+ try:
177
+ stats = basic_stats(graph)
178
+ return {"status": "success", "stats": stats}
179
+ except Exception as e:
180
+ return {"status": "error", "message": str(e)}
181
+
182
+ # -------------------- Graph Visualization Methods --------------------
183
+
184
+ def plot_graph(self, graph, **kwargs):
185
+ """
186
+ Plot a graph.
187
+
188
+ :param graph: Graph to plot.
189
+ :param kwargs: Additional plotting parameters.
190
+ :return: Dictionary with status and plot or error message.
191
+ """
192
+ try:
193
+ plot = plot_graph(graph, **kwargs)
194
+ return {"status": "success", "plot": plot}
195
+ except Exception as e:
196
+ return {"status": "error", "message": str(e)}
197
+
198
+ # -------------------- Graph I/O Methods --------------------
199
+
200
+ def save_graphml(self, graph, filepath):
201
+ """
202
+ Save a graph to GraphML format.
203
+
204
+ :param graph: Graph to save.
205
+ :param filepath: Path to save the GraphML file.
206
+ :return: Dictionary with status or error message.
207
+ """
208
+ try:
209
+ save_graphml(graph, filepath)
210
+ return {"status": "success"}
211
+ except Exception as e:
212
+ return {"status": "error", "message": str(e)}
213
+
214
+ def load_graphml(self, filepath):
215
+ """
216
+ Load a graph from GraphML format.
217
+
218
+ :param filepath: Path to the GraphML file.
219
+ :return: Dictionary with status and loaded graph or error message.
220
+ """
221
+ try:
222
+ graph = load_graphml(filepath)
223
+ return {"status": "success", "graph": graph}
224
+ except Exception as e:
225
+ return {"status": "error", "message": str(e)}
226
+
227
+ # -------------------- Fallback Handling --------------------
228
+
229
+ def handle_import_failure(self):
230
+ """
231
+ Handle import failure gracefully.
232
+
233
+ :return: Dictionary with status and error message.
234
+ """
235
+ if not import_success:
236
+ return {"status": "error", "message": f"Failed to import OSMnx: {import_error}. Please ensure the package is installed correctly."}
237
+ return {"status": "success"}
238
+
239
+ # Example usage:
240
+ # adapter = Adapter()
241
+ # result = adapter.create_graph_from_place("Piedmont, California, USA")
242
+ # print(result)
osmnx/mcp_output/mcp_plugin/main.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MCP Service Auto-Wrapper - Auto-generated
3
+ """
4
+ from mcp_service import create_app
5
+
6
+ def main():
7
+ """Main entry point"""
8
+ app = create_app()
9
+ return app
10
+
11
+ if __name__ == "__main__":
12
+ app = main()
13
+ app.run()
osmnx/mcp_output/mcp_plugin/mcp_service.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+
4
+ # Add the local source directory to sys.path
5
+ source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source")
6
+ if source_path not in sys.path:
7
+ sys.path.insert(0, source_path)
8
+
9
+ from fastmcp import FastMCP
10
+ from osmnx import graph, routing, plot, stats
11
+
12
+ mcp = FastMCP("osmnx_service")
13
+
14
+ @mcp.tool(name="create_graph_from_place", description="Create a graph from a place name")
15
+ def create_graph_from_place(place_name: str, network_type: str) -> dict:
16
+ """
17
+ Create a graph from a place name.
18
+
19
+ :param place_name: The name of the place to create the graph from.
20
+ :param network_type: The type of network to create (e.g., 'walk', 'bike', 'drive').
21
+ :return: A dictionary with success, result, or error fields.
22
+ """
23
+ try:
24
+ G = graph.graph_from_place(place_name, network_type=network_type)
25
+ return {"success": True, "result": G, "error": None}
26
+ except Exception as e:
27
+ return {"success": False, "result": None, "error": str(e)}
28
+
29
+ @mcp.tool(name="shortest_path", description="Find the shortest path between two nodes")
30
+ def shortest_path(G, origin: int, destination: int) -> dict:
31
+ """
32
+ Find the shortest path between two nodes in a graph.
33
+
34
+ :param G: The graph to search.
35
+ :param origin: The origin node ID.
36
+ :param destination: The destination node ID.
37
+ :return: A dictionary with success, result, or error fields.
38
+ """
39
+ try:
40
+ path = routing.shortest_path(G, origin, destination)
41
+ return {"success": True, "result": path, "error": None}
42
+ except Exception as e:
43
+ return {"success": False, "result": None, "error": str(e)}
44
+
45
+ @mcp.tool(name="plot_graph", description="Plot a graph")
46
+ def plot_graph(G) -> dict:
47
+ """
48
+ Plot a graph.
49
+
50
+ :param G: The graph to plot.
51
+ :return: A dictionary with success, result, or error fields.
52
+ """
53
+ try:
54
+ fig, ax = plot.plot_graph(G)
55
+ return {"success": True, "result": (fig, ax), "error": None}
56
+ except Exception as e:
57
+ return {"success": False, "result": None, "error": str(e)}
58
+
59
+ @mcp.tool(name="calculate_basic_stats", description="Calculate basic statistics of a graph")
60
+ def calculate_basic_stats(G) -> dict:
61
+ """
62
+ Calculate basic statistics of a graph.
63
+
64
+ :param G: The graph to analyze.
65
+ :return: A dictionary with success, result, or error fields.
66
+ """
67
+ try:
68
+ stats_result = stats.basic_stats(G)
69
+ return {"success": True, "result": stats_result, "error": None}
70
+ except Exception as e:
71
+ return {"success": False, "result": None, "error": str(e)}
72
+
73
+ def create_app() -> FastMCP:
74
+ """
75
+ Create and return the FastMCP application instance.
76
+
77
+ :return: The FastMCP instance.
78
+ """
79
+ return mcp
osmnx/mcp_output/requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastmcp
2
+ fastapi
3
+ uvicorn[standard]
4
+ pydantic>=2.0.0
5
+ geopandas>=1.0.1
6
+ networkx>=2.5
7
+ numpy>=1.24
8
+ pandas>=1.5
9
+ requests>=2.27
10
+ shapely>=2.0
11
+ matplotlib
osmnx/mcp_output/start_mcp.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ """
3
+ MCP Service Startup Entry
4
+ """
5
+ import sys
6
+ import os
7
+
8
+ project_root = os.path.dirname(os.path.abspath(__file__))
9
+ mcp_plugin_dir = os.path.join(project_root, "mcp_plugin")
10
+ if mcp_plugin_dir not in sys.path:
11
+ sys.path.insert(0, mcp_plugin_dir)
12
+
13
+ from mcp_service import create_app
14
+
15
+ def main():
16
+ """Start FastMCP service"""
17
+ app = create_app()
18
+ # Use environment variable to configure port, default 8000
19
+ port = int(os.environ.get("MCP_PORT", "8000"))
20
+
21
+ # Choose transport mode based on environment variable
22
+ transport = os.environ.get("MCP_TRANSPORT", "stdio")
23
+ if transport == "http":
24
+ app.run(transport="http", host="0.0.0.0", port=port)
25
+ else:
26
+ # Default to STDIO mode
27
+ app.run()
28
+
29
+ if __name__ == "__main__":
30
+ main()
osmnx/mcp_output/workflow_summary.json ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "repository": {
3
+ "name": "osmnx",
4
+ "url": "https://github.com/gboeing/osmnx",
5
+ "local_path": "/export/zxcpu1/shiweijie/code/ghh/Code2MCP/workspace/osmnx",
6
+ "description": "Python library",
7
+ "features": "Basic functionality",
8
+ "tech_stack": "Python",
9
+ "stars": 0,
10
+ "forks": 0,
11
+ "language": "Python",
12
+ "last_updated": "",
13
+ "complexity": "medium",
14
+ "intrusiveness_risk": "low"
15
+ },
16
+ "execution": {
17
+ "start_time": 1770358542.3952017,
18
+ "end_time": 1770358630.1692348,
19
+ "duration": 87.7740330696106,
20
+ "status": "success",
21
+ "workflow_status": "success",
22
+ "nodes_executed": [
23
+ "download",
24
+ "analysis",
25
+ "env",
26
+ "generate",
27
+ "run",
28
+ "review",
29
+ "finalize"
30
+ ],
31
+ "total_files_processed": 1,
32
+ "environment_type": "unknown",
33
+ "llm_calls": 0,
34
+ "deepwiki_calls": 0
35
+ },
36
+ "tests": {
37
+ "original_project": {
38
+ "passed": false,
39
+ "details": {},
40
+ "test_coverage": "100%",
41
+ "execution_time": 0,
42
+ "test_files": []
43
+ },
44
+ "mcp_plugin": {
45
+ "passed": true,
46
+ "details": {},
47
+ "service_health": "healthy",
48
+ "startup_time": 0,
49
+ "transport_mode": "stdio",
50
+ "fastmcp_version": "unknown",
51
+ "mcp_version": "unknown"
52
+ }
53
+ },
54
+ "analysis": {
55
+ "structure": {
56
+ "packages": [
57
+ "source.osmnx"
58
+ ]
59
+ },
60
+ "dependencies": {
61
+ "has_environment_yml": false,
62
+ "has_requirements_txt": false,
63
+ "pyproject": true,
64
+ "setup_cfg": false,
65
+ "setup_py": false
66
+ },
67
+ "entry_points": {
68
+ "imports": [],
69
+ "cli": [],
70
+ "modules": []
71
+ },
72
+ "risk_assessment": {
73
+ "import_feasibility": 0.9,
74
+ "intrusiveness_risk": "low",
75
+ "complexity": "medium"
76
+ },
77
+ "deepwiki_analysis": {
78
+ "repo_url": "https://github.com/gboeing/osmnx",
79
+ "repo_name": "osmnx",
80
+ "content": "gboeing/osmnx\nInstallation and Setup\nGetting Started\nArchitecture and Data Flow\nData Acquisition\nConfiguration and Settings\nCore Functionality\nGraph Creation and Manipulation\nNetwork Analysis\nRouting and Path Finding\nVisualization\nAdvanced Features\nElevation Data\nProjection and Coordinate Systems\nData Import and Export\nDevelopment and Contributing\nTesting and Quality Assurance\nCI/CD Pipeline\nDocker Environment\nVersion History and Roadmap\nCHANGELOG.md\nLICENSE.txt\ndocs/source/conf.py\ndocs/source/index.rst\ndocs/source/internals-reference.rst\ndocs/source/user-reference.rst\nosmnx/__init__.py\nosmnx/_version.py\nOSMnx is a Python package for downloading, modeling, analyzing, and visualizing street networks and other geospatial features from OpenStreetMap (OSM). It provides a simple interface to retrieve complex urban data and convert it into network graphs and other usable formats for spatial analysis. This document introduces the OSMnx system architecture and its core functionality. For installation instructions, seeInstallation and Setup, and for a quick start guide, seeGetting Started.\nPurpose and Scope\nOSMnx enables users to:\nDownload and model walking, driving, or biking networks with a single line of code\nWork with urban amenities/points of interest, building footprints, and transit infrastructure\nAnalyze network metrics and spatial characteristics\nIncorporate elevation data for 3D network analysis and grade calculations\nCalculate and visualize street orientation patterns and entropy\nPerform routing and trip planning with customizable impedance values\nProject and visualize networks and analysis results\nSources:README.md10-12docs/source/index.rst4-5\nSystem Architecture\nCore Components of OSMnx\nOSMnx SystemUser ApplicationOSMnx APIData Acquisition LayerProcessing LayerAnalysis LayerVisualization LayerI/O LayerOverpass API Client(_overpass)Nominatim API Client(_nominatim)HTTP & Caching(_http)graph.py(Network Creation)projection.py(Coordinate Systems)simplification.py(Network Cleanup)convert.py(Data Conversion)distance.py(Distance Calculations)routing.py(Path Finding)stats.py(Network Metrics)bearing.py(Street Orientations)elevation.py(Elevation Data)plot.py(Map Visualization)io.py(Save/Load Graphs)_osm_xml.py(XML Handling)settings.py(Configuration)utils.py(Utilities)OpenStreetMapData\nOSMnx System\nUser Application\nData Acquisition Layer\nProcessing Layer\nAnalysis Layer\nVisualization Layer\nOverpass API Client(_overpass)\nNominatim API Client(_nominatim)\nHTTP & Caching(_http)\ngraph.py(Network Creation)\nprojection.py(Coordinate Systems)\nsimplification.py(Network Cleanup)\nconvert.py(Data Conversion)\ndistance.py(Distance Calculations)\nrouting.py(Path Finding)\nstats.py(Network Metrics)\nbearing.py(Street Orientations)\nelevation.py(Elevation Data)\nplot.py(Map Visualization)\nio.py(Save/Load Graphs)\n_osm_xml.py(XML Handling)\nsettings.py(Configuration)\nutils.py(Utilities)\nOpenStreetMapData\nSources:osmnx/__init__.py10-53docs/source/user-reference.rst\nOutputAnalysisGraph ProcessingData AcquisitionInput MethodsUser InputDefine Query Typegraph_from_place()graph_from_address()graph_from_point()graph_from_bbox()graph_from_polygon()graph_from_xml()geocoder.py_nominatim.py_overpass.py_http.pyOpenStreetMapData_osm_xml.pyNetworkX MultiDiGraphproject_graph()simplify_graph()consolidate_intersections()Add Attributes(speed, travel time,elevation, bearings)Path Finding(shortest_path,k_shortest_paths)Network Analysis(basic_stats)plot_graph()plot_graph_route()save_graphml()save_graph_geopackage()save_graph_xml()\nGraph Processing\nData Acquisition\nInput Methods\nDefine Query Type\ngraph_from_place()\ngraph_from_address()\ngraph_from_point()\ngraph_from_bbox()\ngraph_from_polygon()\ngraph_from_xml()\ngeocoder.py\n_nominatim.py\n_overpass.py\nOpenStreetMapData\n_osm_xml.py\nNetworkX MultiDiGraph\nproject_graph()\nsimplify_graph()\nconsolidate_intersections()\nAdd Attributes(speed, travel time,elevation, bearings)\nPath Finding(shortest_path,k_shortest_paths)\nNetwork Analysis(basic_stats)\nplot_graph()plot_graph_route()\nsave_graphml()save_graph_geopackage()save_graph_xml()\nSources:osmnx/__init__.py10-53CHANGELOG.md\nCore Functionality\nGraph Creation\nOSMnx provides multiple functions for downloading and creating street network graphs:\ngraph_from_place()\ngraph_from_address()\ngraph_from_point()\ngraph_from_bbox()\ngraph_from_polygon()\ngraph_from_xml()\nSources:osmnx/__init__.py27-32\nNetwork Types\nOSMnx can retrieve different types of street networks depending on thenetwork_typeparameter:\nnetwork_type\nNetwork Typeswalk(Pedestrian)bike(Cycling)drive(Driving)drive_service(Driving + Service Roads)all(All Public Ways)all_public(All Public Ways)\nNetwork Types\nwalk(Pedestrian)\nbike(Cycling)\ndrive(Driving)\ndrive_service(Driving + Service Roads)\nall(All Public Ways)\nall_public(All Public Ways)\nSources:CHANGELOG.md79-81\nData Processing and Analysis\nAfter creating a graph, OSMnx provides tools for processing, enriching, and analyzing the network:\nproject_graph()\nsimplify_graph()\nconsolidate_intersections()\nadd_edge_lengths()\nadd_edge_speeds()\nadd_edge_travel_times()\nadd_edge_bearings()\nadd_edge_grades()\nadd_node_elevations_google()\nadd_node_elevations_raster()\nshortest_path()\nk_shortest_paths()\nbasic_stats()\nSources:osmnx/__init__.py10-53CHANGELOG.md\nVisualization\nOSMnx provides multiple plotting functions to visualize networks and analysis results:\nplot_graph()\nplot_graph_route()\nplot_graph_routes()\nplot_footprints()\nplot_figure_ground()\nplot_orientation()\nSources:osmnx/__init__.py37-42\nOSMnx can save and load graphs in various formats:\nsave_graphml()\nload_graphml()\nsave_graph_geopackage()\nsave_graph_xml()\nSources:osmnx/__init__.py33-36\nVersion and Development\nOSMnx is currently at version 2.0.2, released under the MIT license. The package is actively maintained with regular updates and improvements. For version history and roadmap information, seeVersion History and Roadmap.\nSources:osmnx/_version.py3LICENSE.txtCHANGELOG.md\nRefresh this wiki\nOn this page\nPurpose and Scope\nSystem Architecture\nCore Components of OSMnx\nCore Functionality\nGraph Creation\nNetwork Types\nData Processing and Analysis\nVisualization\nVersion and Development",
81
+ "model": "gpt-4o-2024-08-06",
82
+ "source": "selenium",
83
+ "success": true
84
+ },
85
+ "code_complexity": {
86
+ "cyclomatic_complexity": "medium",
87
+ "cognitive_complexity": "medium",
88
+ "maintainability_index": 75
89
+ },
90
+ "security_analysis": {
91
+ "vulnerabilities_found": 0,
92
+ "security_score": 85,
93
+ "recommendations": []
94
+ }
95
+ },
96
+ "plugin_generation": {
97
+ "files_created": [
98
+ "mcp_output/start_mcp.py",
99
+ "mcp_output/mcp_plugin/__init__.py",
100
+ "mcp_output/mcp_plugin/mcp_service.py",
101
+ "mcp_output/mcp_plugin/adapter.py",
102
+ "mcp_output/mcp_plugin/main.py",
103
+ "mcp_output/requirements.txt",
104
+ "mcp_output/README_MCP.md"
105
+ ],
106
+ "main_entry": "start_mcp.py",
107
+ "requirements": [
108
+ "fastmcp>=0.1.0",
109
+ "pydantic>=2.0.0"
110
+ ],
111
+ "readme_path": "/export/zxcpu1/shiweijie/code/ghh/Code2MCP/workspace/osmnx/mcp_output/README_MCP.md",
112
+ "adapter_mode": "import",
113
+ "total_lines_of_code": 0,
114
+ "generated_files_size": 0,
115
+ "tool_endpoints": 0,
116
+ "supported_features": [
117
+ "Basic functionality"
118
+ ],
119
+ "generated_tools": [
120
+ "Basic tools",
121
+ "Health check tools",
122
+ "Version info tools"
123
+ ]
124
+ },
125
+ "code_review": {},
126
+ "errors": [],
127
+ "warnings": [],
128
+ "recommendations": [
129
+ "Improve test coverage by adding more unit tests",
130
+ "Ensure all dependencies are clearly defined in a requirements.txt or environment.yml file",
131
+ "Optimize large files like osmnx/plot.py and osmnx/simplification.py for better performance",
132
+ "Enhance documentation for core modules and functions",
133
+ "Implement continuous integration to automate testing and deployment",
134
+ "Review and refactor code for better readability and maintainability",
135
+ "Consider adding more CLI commands for ease of use",
136
+ "Update the README to include more detailed setup and usage instructions",
137
+ "Regularly update the CHANGELOG to reflect recent changes and improvements",
138
+ "Conduct a code review to identify potential areas for optimization and bug fixes."
139
+ ],
140
+ "performance_metrics": {
141
+ "memory_usage_mb": 0,
142
+ "cpu_usage_percent": 0,
143
+ "response_time_ms": 0,
144
+ "throughput_requests_per_second": 0
145
+ },
146
+ "deployment_info": {
147
+ "supported_platforms": [
148
+ "Linux",
149
+ "Windows",
150
+ "macOS"
151
+ ],
152
+ "python_versions": [
153
+ "3.8",
154
+ "3.9",
155
+ "3.10",
156
+ "3.11",
157
+ "3.12"
158
+ ],
159
+ "deployment_methods": [
160
+ "Docker",
161
+ "pip",
162
+ "conda"
163
+ ],
164
+ "monitoring_support": true,
165
+ "logging_configuration": "structured"
166
+ },
167
+ "execution_analysis": {
168
+ "success_factors": [
169
+ "Successful execution of all workflow nodes",
170
+ "Healthy service status of the MCP plugin"
171
+ ],
172
+ "failure_reasons": [],
173
+ "overall_assessment": "excellent",
174
+ "node_performance": {
175
+ "download_time": "Efficient, completed without issues",
176
+ "analysis_time": "Completed successfully, medium complexity",
177
+ "generation_time": "Efficient, generated necessary files",
178
+ "test_time": "Original project tests failed, MCP plugin tests passed"
179
+ },
180
+ "resource_usage": {
181
+ "memory_efficiency": "Not explicitly measured, assumed efficient due to lack of issues",
182
+ "cpu_efficiency": "Not explicitly measured, assumed efficient due to lack of issues",
183
+ "disk_usage": "Minimal, as only one file was processed"
184
+ }
185
+ },
186
+ "technical_quality": {
187
+ "code_quality_score": 75,
188
+ "architecture_score": 80,
189
+ "performance_score": 70,
190
+ "maintainability_score": 75,
191
+ "security_score": 85,
192
+ "scalability_score": 70
193
+ }
194
+ }
osmnx/source/.pre-commit-config.yaml ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v6.0.0
4
+ hooks:
5
+ - id: check-added-large-files
6
+ args: [--maxkb=50]
7
+ - id: check-ast
8
+ - id: check-case-conflict
9
+ - id: check-executables-have-shebangs
10
+ - id: check-json
11
+ - id: check-merge-conflict
12
+ args: [--assume-in-merge]
13
+ - id: check-shebang-scripts-are-executable
14
+ - id: check-toml
15
+ - id: check-xml
16
+ - id: check-yaml
17
+ - id: detect-private-key
18
+ - id: end-of-file-fixer
19
+ - id: fix-byte-order-marker
20
+ - id: mixed-line-ending
21
+ - id: no-commit-to-branch
22
+ - id: pretty-format-json
23
+ args: [--autofix]
24
+ - id: trailing-whitespace
25
+
26
+ - repo: https://github.com/adrienverge/yamllint
27
+ rev: v1.38.0
28
+ hooks:
29
+ - id: yamllint
30
+ args: [--strict, --config-file=./tests/.yamllint.yml]
31
+
32
+ - repo: https://github.com/numpy/numpydoc
33
+ rev: v1.10.0
34
+ hooks:
35
+ - id: numpydoc-validation
36
+
37
+ - repo: https://github.com/astral-sh/ruff-pre-commit
38
+ rev: v0.14.13
39
+ hooks:
40
+ - id: ruff-check
41
+ args: [--fix, --show-fixes]
42
+ - id: ruff-format
43
+
44
+ - repo: https://github.com/pre-commit/mirrors-mypy
45
+ rev: v1.19.1
46
+ hooks:
47
+ - id: mypy
48
+ additional_dependencies:
49
+ - matplotlib
50
+ - pandas-stubs
51
+ - pytest
52
+ - scipy-stubs
53
+ - types-requests
osmnx/source/CHANGELOG.md ADDED
@@ -0,0 +1,690 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Changelog
2
+
3
+ ## 2.1.0 (TBD)
4
+
5
+ - add Python 3.14 support (#1336)
6
+ - drop Python 3.9 and 3.10 support (#1322 #1336)
7
+ - add validation functions to verify that a graph or GeoDataFrame satisfies OSMnx expectations (#1317)
8
+
9
+ ## 2.0.7 (2025-11-25)
10
+
11
+ - fix TypeError in _getaddrinfo wrapper when host passed both positionally and as keyword (#1340)
12
+ - allow interpolate_points function to run on MultiLineString input (#1341)
13
+
14
+ ## 2.0.6 (2025-08-11)
15
+
16
+ - handle relations with missing member geometries when loading features from XML (#1334)
17
+ - exclude ways tagged "rest_area" or "services" when downloading network data (#1328)
18
+
19
+ ## 2.0.5 (2025-07-05)
20
+
21
+ - fix bug that prevents querying or projecting in polar regions (#1324 #1326)
22
+ - improve module exposure for better code introspection (#1308)
23
+ - add "all" optional dependency extra (#1313)
24
+ - bump minimum required patch versions of optional extras to earliest versions with macosx/arm64 wheels (#1313)
25
+
26
+ ## 2.0.4 (2025-06-11)
27
+
28
+ - fix bug in features module when elements have pre-existing geometry tags (#1298)
29
+ - fix bug in save_graphml function where Gephi compatibility mode erases node attributes (#1300)
30
+ - bump minimum required minor versions of optional extras to earliest versions with linux/amd64 wheels (#1296)
31
+
32
+ ## 2.0.3 (2025-05-06)
33
+
34
+ - ensure geocoder results are sorted by importance (#1290)
35
+ - update official reference paper and citations (#1293)
36
+
37
+ ## 2.0.2 (2025-03-25)
38
+
39
+ - fix bug in parsing time when calculating pause duration between requests (#1277)
40
+ - fix bug in round-robin DNS resolution by safely handling missing host argument (#1282)
41
+ - fix bug where consolidate_intersections function would mutate the passed-in graph (#1273)
42
+ - add graph-level "consolidated" attribute if consolidate_intersections has been run (#1273)
43
+ - provide user-friendly error message if consolidate_intersections is run more than once (#1273)
44
+ - refactor internals of caching and pausing between requests (#1279)
45
+ - streamline internal handling of radians throughout package (#1281)
46
+ - improve docstrings (#1272 #1274)
47
+
48
+ ## 2.0.1 (2025-01-01)
49
+
50
+ - fix error message when elevation module's optional dependencies are missing (#1250)
51
+ - update "walk" network_type to filter out ways whose sidewalks are mapped separately (#1254)
52
+
53
+ ## 2.0.0 (2024-11-24)
54
+
55
+ Read the v2 [migration guide](https://github.com/gboeing/osmnx/issues/1123)
56
+
57
+ - add type annotations to all public and private functions throughout package (#1107)
58
+ - remove all functionality previously deprecated in v1 (#1113 #1122 #1135 #1148)
59
+ - add Python 3.13 support (#1223)
60
+ - drop Python 3.8 support (#1106)
61
+ - bump minimum required numpy version to 1.22 for typing support (#1133 #1198)
62
+ - bump minimum required versions of geopandas to 1.0 and pandas to 1.4 for union_all support (#1179 #1198)
63
+ - replace gdal optional dependency with rio-vrt optional dependency (#1203)
64
+ - improve docstrings throughout package (#1116)
65
+ - improve logging and warnings throughout package (#1125)
66
+ - improve error messages throughout package (#1131)
67
+ - improve internal file handling context management (#1226 #1227)
68
+ - refactor features module for speed improvement and memory efficiency (#1157 #1205)
69
+ - refactor save_graph_xml function and \_osm_xml module for speed improvement and bug fixes (#1135)
70
+ - make save_graph_xml function accept only an unsimplified MultiDiGraph as its input data (#1135)
71
+ - replace save_graph_xml function's edge_tag_aggs tuple parameter with way_tag_aggs dict parameter (#1135)
72
+ - add utils_geo.buffer_geometry helper function (#1214)
73
+ - add OSM junction and railway tags to the default settings.useful_tags_node (#1144)
74
+ - add node_attrs_include argument to simplification.simplify_graph function to flexibly relax strictness (#1145)
75
+ - add edge_attr_aggs argument to simplify_graph function to specify aggregation behavior (#1155)
76
+ - add node_attr_aggs argument to the consolidate_intersections function to specify aggregation behavior (#1155)
77
+ - allow per-node tolerance values for intersection consolidation (#1160)
78
+ - make consolidate_intersections function retain unique attribute values when consolidating nodes (#1144)
79
+ - make which_result function parameters consistently able to accept a list throughout package (#1113)
80
+ - handle implicit maxspeed values in add_edge_speeds function (#1153)
81
+ - change add_node_elevations_google default batch_size to 512 to match Google's limit (#1115)
82
+ - better virtual raster handling in elevation module (#1236)
83
+ - use system's default start method when multiprocessing (#1237)
84
+ - allow analysis of MultiDiGraph directional edge bearings and orientation (#1139)
85
+ - allow graph union queries through the custom_filter argument (#1204)
86
+ - fix graph projection creating useless lat and lon node attributes (#1144)
87
+ - fix bug in \_downloader.\_save_to_cache function usage (#1107)
88
+ - fix bug in handling requests ConnectionError when querying Overpass status endpoint (#1113)
89
+ - fix minor bugs throughout to address inconsistencies revealed by type enforcement (#1107 #1114)
90
+ - make optional function parameters keyword-only throughout package (#1134)
91
+ - make dist function parameters required rather than optional throughout package (#1134)
92
+ - make utils_geo.bbox_from_point function return a tuple of floats for consistency with rest of package (#1113)
93
+ - make bounding box coordinate order consistently left, bottom, right, top (#1196)
94
+ - rename truncate.truncate_graph_dist max_dist argument to dist for consistency with rest of package (#1134)
95
+ - remove retain_all argument from all truncate module functions (#1148)
96
+ - remove settings module's deprecated and now replaced settings (#1129 #1136)
97
+ - rename osm_xml module to \_osm_xml to make it private, as all its functions are private (#1113)
98
+ - rename private \_downloader module to \_http (#1114)
99
+ - remove unnecessary private \_api module (#1114)
100
+
101
+ ## 1.9.4 (2024-07-24)
102
+
103
+ - pin maximum dependency versions for remaining v1 releases
104
+ - add warning to note that the order of bounding box coordinates will change in v2
105
+
106
+ ## 1.9.3 (2024-05-01)
107
+
108
+ - update the official package reference paper (#1169)
109
+ - rename network_types "all" -> "all_public" and "all_private" -> "all" for clarity (#1164)
110
+ - deprecate the obsolete "all_private" network_type name (#1164)
111
+
112
+ ## 1.9.2 (2024-04-02)
113
+
114
+ - deprecate and replace settings module's default_accept_language, default_referer, and default_user_agent settings (#1138)
115
+ - deprecate and replace settings module's memory, nominatim_endpoint, overpass_endpoint, and timeout settings (#1138)
116
+ - deprecate save_graph_xml function's renamed or obsolete parameters (#1138)
117
+ - deprecate graph_from_xml tags and polygon function parameters (#1146)
118
+ - deprecate simplify_graph function's endpoint_attrs argument and replace it with edge_attrs_differ (#1146)
119
+ - deprecate utils_graph.get_digraph function and replace it with covert.to_digraph function (#1146)
120
+ - deprecate utils_graph.get_undirected function and replace it with covert.to_undirected function (#1146)
121
+ - deprecate utils_graph.graph_to_gdfs function and replace it with covert.graph_to_gdfs function (#1146)
122
+ - deprecate utils_graph.graph_from_gdfs function and replace it with covert.graph_from_gdfs function (#1146)
123
+ - deprecate utils_graph.remove_isolated_nodes function (#1156)
124
+ - deprecate utils_graph.get_largest_component function and replace it with truncate.largest_component function (#1146)
125
+ - deprecate utils_graph.route_to_gdf function and replace it with routing.route_to_gdf function (#1146)
126
+ - deprecate speed module and move all of its functionality to the routing module (#1146)
127
+
128
+ ## 1.9.1 (2024-02-01)
129
+
130
+ - fix deprecation warning in simplification.simplify_graph function (#1126)
131
+
132
+ ## 1.9.0 (2024-01-31)
133
+
134
+ - add endpoint_attrs argument to simplification.simplify_graph function to flexibly relax strictness (#1117)
135
+ - fix a bug in the features module's polygon handling (#1104)
136
+ - update obsolete numpy random number generation (#1108)
137
+ - make deprecation warnings FutureWarnings (#1124)
138
+ - update warning messages to note that deprecated code will be removed in v2.0.0 (#1111)
139
+ - deprecate strict argument in simplification.simplify_graph function in favor of new endpoint_attrs argument (#1117)
140
+ - deprecate north, south, east, west arguments throughout package in favor of bbox tuple argument (#1112)
141
+ - deprecate return_coords argument in graph.graph_from_address function (#1105)
142
+ - deprecate return_hex argument in plot.get_colors function (#1109)
143
+ - deprecate address, point, network_type, edge_color, and smooth_joints arguments in plot.plot_figure_ground function (#1121)
144
+
145
+ ## 1.8.1 (2023-12-31)
146
+
147
+ - fix a bug arising from the save_graph_xml function (#1093)
148
+ - warn user if their query area is significantly larger than max query area size (#1101)
149
+ - refactor utils_geo module and deprecate quadrat_width and min_num function arguments (#1100)
150
+ - under-the-hood code clean-up (#1092 #1099 #1103)
151
+
152
+ ## 1.8.0 (2023-11-30)
153
+
154
+ - formally support Python 3.12 (#1082)
155
+ - fix Windows-specific character encoding issue when reading XML files (#1084)
156
+ - resolve pandas and gdal future warnings (#1089)
157
+ - use spawn instead of fork for multiprocessing to resolve Python 3.12 deprecation warning (#1089)
158
+ - rename add_node_elevations_google function's max_locations_per_batch parameter, with deprecation warning (#1088)
159
+ - move add_node_elevations_google function's url_template parameter to settings module, with deprecation warning (#1088)
160
+
161
+ ## 1.7.1 (2023-10-29)
162
+
163
+ - fix references to latitude and longitude parameters as lat and lon consistently across package (#1068 #1069)
164
+ - fix handling of dict and set attribute types when reloading GraphML files (#1075 #1077)
165
+
166
+ ## 1.7.0 (2023-10-11)
167
+
168
+ - improve automatic UTM handling in the projection module (#1059)
169
+ - add a to_latlong parameter to the projection.project_graph function for API consistency (#1057)
170
+ - workaround for pytest issue with printing to terminal window on Windows (#1064)
171
+ - refactor the distance module and add a new routing module (#1063)
172
+ - move shortest_path and k_shortest_paths functions to new routing module, with deprecation warning (#1063)
173
+ - rename great_circle_vec and euclidean_dist_vec functions to great_circle and euclidean, with deprecation warning (#1063)
174
+ - under-the-hood code clean-up (#1047)
175
+
176
+ ## 1.6.0 (2023-07-28)
177
+
178
+ - fix DNS resolution in Dask clusters (#1039)
179
+ - improve memory efficiency during features GeoDataFrame creation (#1043)
180
+ - handle the settings.cache_only_mode option in the features module (#1043)
181
+ - deprecate the buffer_dist and clean_periphery function parameters throughout package (#1044)
182
+ - add more descriptive exceptions: ResponseStatusCodeError and GraphSimplificationError (#1041)
183
+ - replace CacheOnlyModeInterrupt exception with CacheOnlyInterruptError exception (#1041)
184
+ - replace EmptyOverpassResponse exception with InsufficientResponseError exception (#1041)
185
+ - refactor elevation module (#1042 #1043)
186
+ - refactor the \_downloader module and add new \_overpass and \_nominatim modules (#1043)
187
+ - under-the-hood code clean-up (#1036 #1037 #1038)
188
+
189
+ ## 1.5.1 (2023-07-08)
190
+
191
+ - improve memory efficiency during graph creation (#1021 #1029)
192
+ - improve log messaging (#1032)
193
+ - add version number to XML generator attribute in save_graph_xml (#1031)
194
+ - warn user if loading a .osm XML file generated by OSMnx itself (#1031)
195
+ - add style keyword argument to citation function (#1034)
196
+
197
+ ## 1.5.0 (2023-06-28)
198
+
199
+ - fix bug in save_graph_xml due to roundabout ways (#986 #999)
200
+ - fix GeoPandas future warning (#1012)
201
+ - make API key properly optional in elevation.add_node_elevations_google function (#999)
202
+ - rename geometries module as features module and deprecate geometries module (#1007 #1011)
203
+ - remove private \_polygon_features module and move its data to features module (#994)
204
+ - make the internal downloader module private (#1010)
205
+ - deprecate interpolate parameter in distance.nearest_edges function (#1010)
206
+ - move save_graph_xml function to io module with deprecation warning in osm_xml module (#1017)
207
+ - migrate from setup.py, setup.cfg, and requirements.txt to pyproject.toml (#1002)
208
+ - pin optional dependencies to minimum required versions (#995)
209
+ - expand and reorganize the documentation (#993)
210
+
211
+ ## 1.4.0 (2023-06-11)
212
+
213
+ - verify edge weight attribute values before solving shortest paths (#967)
214
+ - provide consistent error when no data elements are returned from Overpass (#960)
215
+ - add route_to_gdf function to utils_graph module to return a GeoDataFrame of the edges in a path (#957)
216
+ - deprecate the get_route_edge_attributes function in favor of the new route_to_gdf function (#957)
217
+ - deprecate folium module in favor of using geopandas.GeoDataFrame.explore directly (#957)
218
+ - deprecate precision parameter in bearing, distance, elevation, and speed modules' functions (#981)
219
+ - deprecate utils_geo.round_geometry_coords function (#981)
220
+ - move plot_orientation function from bearing module to plot module (#956)
221
+ - make matplotlib an optional dependency required only for the plot module (#976)
222
+ - drop pyproj package dependency (#980)
223
+
224
+ ## 1.3.1.post0 (2023-05-26)
225
+
226
+ - restore Python 3.8 compatibility (#965)
227
+
228
+ ## 1.3.1 (2023-05-24)
229
+
230
+ - improve DNS resolution when using proxies or on networks blocking DNS-over-HTTPS (#924 #953)
231
+ - improve processing of per-lane values when adding edge speeds (#944 #955)
232
+ - improve file writing in save_graph_xml function (#917 #961)
233
+ - ensure node coordinates are non-null and convertible to float in the add_edge_lengths function (#950)
234
+ - ignore ways tagged highway=no or highway=razed in built-in filters (#938)
235
+ - do not assume an edge with key=0 exists between each node pair when simplifying graph (#921)
236
+ - drop dateutil package dependency (#919)
237
+
238
+ ## 1.3.0 (2023-01-01)
239
+
240
+ - fully support Shapely 2.0 and drop support for Shapely 1.x (#900)
241
+ - drop RTree package dependency (#900)
242
+ - much faster nearest edges search using STRTree index (#900)
243
+ - allow using alternative Google Maps compatible elevation APIs, such as Open Topo Data (#901 #903)
244
+ - optionally track merged_edges as a new edge attribute in simplify_graph function (#892 #909)
245
+
246
+ ## 1.2.3 (2022-12-14)
247
+
248
+ - fix bug that added unsimplified edge geometry attributes when projecting
249
+ - hard code Google DNS IP address
250
+ - resolve matplotlib deprecation warning
251
+ - deprecate save_graph_shapefile function
252
+
253
+ ## 1.2.2 (2022-08-05)
254
+
255
+ - fix compatibility with rasterio 1.3
256
+ - fix API version when saving OSM XML
257
+ - resolve shapely deprecation warning
258
+
259
+ ## 1.2.1 (2022-06-16)
260
+
261
+ - fix rate limit checking and pausing on newest versions of Overpass API
262
+ - allow add_edge_lengths function to be run on a subset of edges
263
+ - resolve pandas deprecation warning
264
+
265
+ ## 1.2.0 (2022-05-23)
266
+
267
+ - add ability to load GraphML string data to the load_graphml function
268
+ - add "reversed" edge attribute to support node-order-dependent edge attributes
269
+ - add new edge_color and edge_linewidth arguments to plot_footprints function
270
+ - fix nearest_edges function selecting arbitrary edge when bounding boxes overlap
271
+ - fix get_digraph function's parallel edge handling
272
+ - fix pandas and geopandas version compatibility
273
+ - fix log output appearing in Jupyter notebooks on Unix-like systems
274
+ - remove old functions and arguments previously deprecated in v1.1
275
+ - deprecate utils.config function in favor of using settings module directly
276
+
277
+ ## 1.1.2 (2021-11-17)
278
+
279
+ - fix geocoding when no geojson is returned
280
+ - fix graph simplification to properly handle travel_time edge attributes
281
+ - fix streets per node not being calculated when clean_periphery=False
282
+ - allow user-defined aggregation function when imputing missing edge speeds
283
+ - allow user to configure requests package keyword arguments when connecting to APIs
284
+ - faster graph projection by calculating UTM zone number with a computationally cheaper method
285
+ - improve efficiency of quadrat-based geometry cutting
286
+ - fall back on google dns resolution when necessary if using a proxy
287
+ - move count_streets_per_node function to stats module
288
+ - resolve shapely and geopandas deprecation warnings
289
+
290
+ ## 1.1.1 (2021-05-19)
291
+
292
+ - fix overpass status endpoint checks with explicit IP address resolution
293
+ - fix slot management on local overpass instances by optionally disabling rate limiting
294
+ - parallelize shortest_path calculation for multiple origins/destinations
295
+
296
+ ## 1.1.0 (2021-05-01)
297
+
298
+ - add graph-constrained spatial sampling function
299
+ - add add_node_elevations_raster function to add node elevations from local raster file(s)
300
+ - add add_node_elevations_google function and deprecate old add_node_elevations function
301
+ - add faster streamlined nearest_nodes and nearest_edges functions to distance module
302
+ - deprecate old get_nearest_node, get_nearest_nodes, get_nearest_edge, and get_nearest_edges
303
+ - add utils_geo.interpolate_points function and deprecate redistribute_vertices in favor of it
304
+ - add vectorized calculate_bearing function and deprecate get_bearing in favor of it
305
+ - expose individual street network stats functions in stats module
306
+ - deprecate the extended_stats function in stats module
307
+ - add network orientation and entropy stats functions to bearing module
308
+ - add plot_orientation function to bearing module to polar histograms of graph edge bearings
309
+ - add route_linewidths parameter to plot_graph_routes function
310
+ - handle relations of type "boundary" in geometries module
311
+ - multi-index GeoDataFrames returned from geometries module by element type and osmid
312
+ - ensure all nodes have integer IDs after graph intersection consolidation
313
+ - vectorize add_edge_lengths, add_edge_grades, and add_edge_bearings functions
314
+ - improve save_graph_xml speed
315
+ - improve geocoder module error messages
316
+ - improve handling of node geometry when converting graph to/from GeoDataFrames
317
+ - fix network_type filters allowing ways tagged "bus_guideway"
318
+ - fix handling of boolean type conversion in load_graphml
319
+ - fix truncate_graph_dist retaining unreachable nodes
320
+ - fix bug in consolidate_intersections when pygeos is installed
321
+ - move add_edge_lengths function from utils_graph to distance module
322
+ - remove descartes dependency in line with geopandas
323
+
324
+ ## 1.0.1 (2021-01-13)
325
+
326
+ - fix network_type filters allowing ways tagged "planned"
327
+ - fix "drive" network_type allowing some alleys
328
+ - fix intersection consolidation for compatibility with v1.0 node ids/indexing
329
+ - fix python 3.6 compatibility
330
+ - deprecate folium polyline styling arguments
331
+
332
+ ## 1.0.0 (2021-01-01)
333
+
334
+ - set use_cache=True by default
335
+ - add ability to query a place by OSM ID in geocoder.geocode_to_gdf function
336
+ - add optional setting for download/cache-only mode
337
+ - replace md5 with sha1 for cache filename hashing
338
+ - replace streets_per_node graph attribute with equivalent street_count node attribute
339
+ - remove redundant osmid node attribute
340
+ - make graph_to_gdfs multi-index the edges GeoDataFrame by u, v, key
341
+ - refactor consolidate_intersections function for better speed and efficiency
342
+ - refactor count_streets_per_node function for better speed and efficiency
343
+ - refactor folium module for better speed and efficiency
344
+ - refactor get_undirected functionality for better speed and efficiency
345
+ - extract all private/internal .osm XML functionality into new osm_xml module
346
+ - deprecate io.save_graph_xml with warning (function moved to osm_xml module)
347
+ - remove internal \_is_simplified function
348
+ - remove deprecated pois module
349
+ - remove deprecated footprints module
350
+ - remove deprecated utils_graph.induce_subgraph function
351
+ - remove deprecated node_type parameter from io.load_graphml function
352
+
353
+ ## 0.16.2 (2020-11-17)
354
+
355
+ - improve graph_from_gdfs speed and efficiency
356
+ - improve plot_route_folium speed and efficiency
357
+ - fix remove_isolated_nodes function mutating the passed-in graph
358
+ - fix gephi compatibility in save_graphml
359
+ - add customizable node/edge attribute data type arguments to load_graphml
360
+ - deprecate old node_type argument in load_graphml
361
+ - expose bidirectional_network_types via config function
362
+
363
+ ## 0.16.1 (2020-10-05)
364
+
365
+ - fix handling graphs with no intersections in consolidate_intersections
366
+ - fix consolidate_intersections returning GeoSeries without CRS attribute
367
+ - fix response caching to save only when status code is 200
368
+ - fix elevation module's grade absolute value calculation when grade is null
369
+ - move shortest path functions from utils_graph module to distance module
370
+
371
+ ## 0.16.0 (2020-09-07)
372
+
373
+ - new geometries module for creating GeoDataFrames from tag/value queries
374
+ - deprecate old pois and footprints modules (replaced by geometries module)
375
+ - auto-select first Polygon/MultiPolygon when geocoding with which_result=None
376
+ - new k*shortest_paths function to solve \_k* shortest paths from origin to destination
377
+ - new shortest_path convenience function
378
+ - new get_digraph function to correctly convert MultiDiGraph to DiGraph
379
+ - miscellaneous performance improvements and optimizations
380
+ - deprecate induce_subgraph function
381
+ - remove deprecated boundaries module (replaced by geocoder module in v0.15.0)
382
+ - remove deprecated utils_geo.geocode function (replaced by geocoder.geocode function in v0.15.0)
383
+
384
+ ## 0.15.1 (2020-07-03)
385
+
386
+ - fix geopandas future warnings
387
+
388
+ ## 0.15.0 (2020-06-30)
389
+
390
+ - improve plotting defaults and streamline plot module speed and efficiency
391
+ - improve color handling in plot module
392
+ - improve route plotting
393
+ - plot_graph_routes function now accepts multiple route colors
394
+ - allow multiple elevation API providers
395
+ - consolidate_intersections replaces update_edge_lengths param with reconnect_edges param
396
+ - fix geopackage file saving after consolidating intersections
397
+ - add new geocoder module and move utils_geo.geocode function into it
398
+ - replace gdf_from_place/s functions with geocoder.geocode_to_gdf
399
+ - deprecate boundaries module
400
+ - remove deprecated timeout, memory, custom_settings, and max_query_area_size function params
401
+ - remove deprecated plotting params and plot_shape function
402
+
403
+ ## 0.14.1 (2020-06-09)
404
+
405
+ - fix simplification of graphs with long rural roads
406
+ - reduce memory footprint of graph simplification
407
+ - remove disconnected self-contained rings from graph by default when simplifying
408
+ - improve speed and efficiency of project_graph, graph_to_gdfs, and graph_from_gdfs
409
+ - improve attribute value conversion in load_graphml
410
+ - expose precision parameter for adding bearings, elevations, speeds, and travel times
411
+ - fix config function clobber behavior
412
+ - fix graph periphery cleaning when clean_periphery=True but simplify=False
413
+ - rename settings useful_tags_path to the more appropriate useful_tags_way
414
+ - deprecate the timeout, memory, custom_settings, and max_query_area_size function params
415
+ - the params above are now accessible via config function and settings module
416
+ - deprecate old plot params and plot_shape function
417
+ - remove previously deprecated infrastructure parameter in favor of custom_filter
418
+
419
+ ## 0.14.0 (2020-06-03)
420
+
421
+ - better geometry subdividing for huge OSM queries
422
+ - better handling of maxspeed list values for simplified graphs
423
+ - downloader only retrieves url response from cache if no server remark
424
+ - deprecate graph creation infrastructure parameter in favor of flexible custom_filter
425
+ - remove deprecated functions: graph_from_file, clean_intersections, gdfs_to_graph
426
+
427
+ ## 0.13.0 (2020-05-25)
428
+
429
+ - major refactor of entire package
430
+ - clean up API and namespace
431
+ - new consolidate_intersections function with topological option
432
+ - new speed module to calculate graph edge speeds and travel times
433
+ - generalize POIs module to query with a flexible tags dict
434
+ - allow folium functions to accept FeatureGroup and kwargs
435
+ - all graph saving functions now take a filepath argument instead of folder/filename
436
+ - save shapefiles in single folder containing both nodes and edges
437
+ - optionally return distance and/or geometry in nearest edge search
438
+ - expose timeout and memory parameters in pois and footprints modules
439
+ - define default crs via epsg code instead of proj4 string
440
+ - update and simplify logging with timestamps
441
+ - graph metadata: add creation date and version, remove name
442
+ - replace inconsistent distance parameters with consistent dist parameters
443
+ - deprecate old clean_intersections function in favor of new consolidate_intersections
444
+ - deprecate old gdfs_to_graph function in favor of graph_from_gdfs
445
+ - deprecate old graph_from_file function in favor of graph_from_xml
446
+ - rename save_as_osm function -> save_graph_xml for consistency
447
+ - rename save_load module -> io
448
+ - remove old save_gdf_shapefile function
449
+ - drop support for python 3.5 and lower
450
+
451
+ ## 0.12.1 (2020-05-01)
452
+
453
+ - fix handling relations with missing type tag
454
+ - fix save_graph_geopackage handling numeric attributes
455
+ - fix load_graphml handling elevation and grade attributes
456
+ - improve edge finding algorithms to return edge key
457
+ - more informative graph_from_file data load error message
458
+ - refactor url-in-cache checking
459
+ - add timestamp helper function
460
+ - documentation improvements
461
+
462
+ ## 0.12 (2020-04-10)
463
+
464
+ - add ability to save graph as geopackage file
465
+ - add truncate_by_edge implementation in truncate_graph_polygon
466
+ - allow flexible overpass settings (e.g., to query by date)
467
+ - better handling of invalid footprint geometries
468
+ - geocode function now uses nominatim_request function
469
+ - improve .osm xml output
470
+ - improve one-way street handling
471
+ - fix graph projection overwriting original lat/lng
472
+ - fix redistribute_vertices function for MultiLineStrings
473
+
474
+ ## 0.11.4 (2020-01-31)
475
+
476
+ - fix .osm xml output
477
+ - fix for pandas 1.0
478
+
479
+ ## 0.11.3 (2020-01-09)
480
+
481
+ - fix errant print statement
482
+
483
+ ## 0.11.2 (2020-01-07)
484
+
485
+ - fix .osm xml output
486
+ - fix geopandas future compatibility
487
+
488
+ ## 0.11.1 (2020-01-01)
489
+
490
+ - fix get_nearest_edges search when not using a spatial index
491
+
492
+ ## 0.11 (2019-12-04)
493
+
494
+ - drop formal python 2 support
495
+ - refactor all modules for cleaner package organization
496
+ - make stats betweenness centrality compatible with networkx>=2.4
497
+ - allow configurable overpass and nominatim endpoints
498
+ - allow gdf_from_places to take a which_result list argument
499
+ - handle zero-division in street grade calculation
500
+ - better footprint relation handling
501
+ - improve network type queries for better filtering
502
+ - fix pois_from_polygon returning points outside polygon
503
+
504
+ ## 0.10 (2019-05-08)
505
+
506
+ - remove deprecated buildings module
507
+ - filter steps ways out of bike queries
508
+ - convert CRS-handling to proj4 strings
509
+ - save graph to xml-formatted .osm file
510
+ - minor refactoring
511
+
512
+ ## 0.9 (2019-01-28)
513
+
514
+ - deprecate buildings module and replace with generalized footprints module
515
+ - improve handling of multipolygon footprints
516
+ - new function to find nearest edge(s), given coordinates
517
+ - add "search," "reverse," and "lookup" nominatim queries
518
+ - use unprojected graphs for figure-ground plotting functions
519
+ - allow non-integer osmid values for custom data
520
+ - improve get_route_edge_attributes function
521
+ - improve color mapping by node/edge attribute value
522
+ - make bidirectional network types explicit
523
+ - networkx compatibility fixes to resolve warnings
524
+
525
+ ## 0.8.2 (2018-09-19)
526
+
527
+ - add python 3.7 compatibility
528
+ - add convenience function to plot several routes over the same map
529
+ - optimize graph truncation to bounding box
530
+ - give self-loops a null bearing when calculating edge bearings
531
+ - make accept-language http header explicit and configurable
532
+ - add citation function
533
+ - refactor POI module
534
+
535
+ ## 0.8.1 (2018-05-17)
536
+
537
+ - add Gephi compatibility argument for saving GraphML
538
+ - handle square bracket encapsulated strings when loading GraphML
539
+
540
+ ## 0.8 (2018-05-05)
541
+
542
+ - add ability to retrieve points of interest
543
+ - improve performance for retrieving huge geographies' street networks
544
+ - fix building footprint retrieval query syntax
545
+ - minor bug fixes
546
+
547
+ ## 0.7.4 (2018-04-05)
548
+
549
+ - add fast nearest-nodes search
550
+ - allow custom network query filters
551
+ - allow create_graph to return graph with no edges
552
+ - improve figure_ground joint smoothing
553
+ - fix handling of parallel edges when making multidigraph undirected
554
+ - generalize same-geometry checker
555
+ - improve detection of prior topology simplification
556
+ - custom error types for finer-grained handling
557
+
558
+ ## 0.7.3 (2018-03-12)
559
+
560
+ - turn off x- and y-axes to improve plotting appearance
561
+ - make floating-point precision and rounding more sensible
562
+ - improve OS path handling cross-platform
563
+ - replace great-circle distance calculator with haversine
564
+ - add access filter as configurable setting
565
+ - improve performance of inducing subgraphs
566
+ - fix utils.get_largest_component for networkx 2.2 compatibility
567
+ - fix config settings namespacing
568
+
569
+ ## 0.7.2 (2018-02-15)
570
+
571
+ - compatibility with networkx 2.1
572
+
573
+ ## 0.7.1 (2018-02-04)
574
+
575
+ - fix documentation build
576
+ - ignore ways marked access=no
577
+
578
+ ## 0.7 (2018-02-01)
579
+
580
+ - ability to load a graph from a .osm file
581
+ - change datum from NAD83 to WGS84
582
+ - make roundabouts one-way
583
+ - conformal plotting for unprojected graphs
584
+ - fix folium web maps rendering
585
+
586
+ ## 0.6 (2017-10-02)
587
+
588
+ - migrate to the networkx 2.0 API
589
+
590
+ ## 0.5.4 (2017-09-16)
591
+
592
+ - add optional cleaned intersections count to basic stats
593
+ - allow circuity to be calculated for projected or unprojected networks
594
+ - various code clean-up and refactoring
595
+
596
+ ## 0.5.3 (2017-07-22)
597
+
598
+ - add requirements files to distribution
599
+
600
+ ## 0.5.2 (2017-07-22)
601
+
602
+ - add ability to download other infrastructures besides just roads/paths (e.g., rail lines, power lines, etc.)
603
+ - calculate graph edges' bearings
604
+ - add ability to get nearest node by great circle or euclidean distance
605
+ - move examples/demo notebooks to new repo: osmnx-examples
606
+ - fix docstrings
607
+ - fix building footprint downloads that require multiple calls for large areas
608
+ - fix missing MultiPolygon import in buildings module
609
+
610
+ ## 0.5.1 (2017-05-12)
611
+
612
+ - functionality to clean-up and consolidate complex intersections
613
+ - let save_gdf_shapefile save building footprint GeoDataFrames
614
+ - set node color correctly in figure-ground diagrams
615
+
616
+ ## 0.5 (2017-04-25)
617
+
618
+ - add elevation module to get node elevations and street grades
619
+ - new color sequence creation and conversion functions in plot module
620
+ - new function to get a path's edge attribute values
621
+ - gracefully handle subpolygons that are invalid or have zero area
622
+ - make truncate_graph_polygon work on projected graphs
623
+ - plot_shape accepts a color or a list of colors
624
+ - make all requests to Overpass API set custom user-agent and referer
625
+ - rewrite algorithms to convert multidigraphs to multigraphs
626
+
627
+ ## 0.4.1 (2017-04-01)
628
+
629
+ - fix load_graphml so we can save a graph again after loading it
630
+ - fix load_graphml so edge oneway attribute is not always set to True
631
+ - buildings module gets buildings stored in OSM as relations as well as ways
632
+ - fix figure-ground diagram saving to make perfect square and smooth joints
633
+ - add optional graph argument to plot_figure_ground
634
+ - suppress jupyter notebook deprecation warnings
635
+
636
+ ## 0.4 (2017-03-01)
637
+
638
+ - plot entire networks with folium
639
+ - plot routes on top of networks with folium
640
+ - vectorize all great circle calculations
641
+ - new geocode function in utils
642
+ - remove geopy dependency
643
+ - refactor modules
644
+ - simplify before truncating by distance when getting graph by point and network distance
645
+ - project geometries, GeoDataFrames, and graphs to a passed-in CRS
646
+
647
+ ## 0.3.1 (2017-02-15)
648
+
649
+ - clean up docstrings throughout
650
+ - remove network code vestiges from buildings.py
651
+
652
+ ## 0.3 (2017-01-29)
653
+
654
+ - add route plotting with folium
655
+ - add downloading and visualization of building footprints
656
+ - updates for compatibility with matplotlib 2.0
657
+
658
+ ## 0.2.2 (2017-01-20)
659
+
660
+ - fixes for compatibility with networkx 2.0's new API
661
+ - make png default image save format
662
+ - figure-ground plots collect street network from a wider area
663
+
664
+ ## 0.2.1 (2017-01-11)
665
+
666
+ - add license file to dist package
667
+
668
+ ## 0.2 (2017-01-10)
669
+
670
+ - refactor modules
671
+ - add graph to GDF and GDF to graph functions
672
+ - add encoding argument to save_graph_shapefile
673
+ - add unit tests and continuous integration
674
+
675
+ ## 0.1 (2016-12-19)
676
+
677
+ - add street width attribute for ways from OSM
678
+
679
+ ## 0.1b2 (2016-11-29)
680
+
681
+ - make simplification error messages explicit
682
+
683
+ ## 0.1b1 (2016-11-28)
684
+
685
+ - process land use and area tags from OSM
686
+ - make intersection error messages clear
687
+
688
+ ## 0.1a1 (2016-11-07)
689
+
690
+ - first pre-release
osmnx/source/CITATION.cff ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ cff-version: 1.2.0
2
+ title: OSMnx
3
+ message: If you use OSMnx, please cite the preferred-citation below.
4
+ type: software
5
+ authors:
6
+ - family-names: Boeing
7
+ given-names: Geoff
8
+ orcid: https://orcid.org/0000-0003-1851-6411
9
+ identifiers:
10
+ - type: doi
11
+ value: 10.1111/gean.70009
12
+ description: Official reference paper
13
+ repository-code: https://github.com/gboeing/osmnx
14
+ url: https://osmnx.readthedocs.org
15
+ abstract: >-
16
+ OSMnx is a Python package to easily download, model, analyze, and visualize
17
+ street networks and other geospatial features from OpenStreetMap.
18
+ license: MIT
19
+ preferred-citation:
20
+ type: article
21
+ authors:
22
+ - family-names: Boeing
23
+ given-names: Geoff
24
+ orcid: https://orcid.org/0000-0003-1851-6411
25
+ year: 2025
26
+ title: Modeling and Analyzing Urban Networks and Amenities with OSMnx
27
+ journal: Geographical Analysis
28
+ volume: 57
29
+ issue: 4
30
+ start: 567
31
+ end: 577
32
+ doi: 10.1111/gean.70009
osmnx/source/CONTRIBUTING.md ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributing guidelines
2
+
3
+ Thanks for using OSMnx and for considering contributing to it by opening an issue or pull request. Every piece of software is a work in progress. This project is the result of many hours of work contributed freely its contributors and the many people that build the projects on which it depends. Thank you for contributing!
4
+
5
+ ## If you have a "how-to" or usage question
6
+
7
+ Please ask your question on [StackOverflow](https://stackoverflow.com/search?q=osmnx), as we reserve the issue tracker for bug reports and new feature development. Any such questions asked in the issue tracker will be automatically closed.
8
+
9
+ ## If you have an installation problem
10
+
11
+ Ensure you have followed the installation instructions in the [documentation](https://osmnx.readthedocs.io/). If you installed OSMnx via conda-forge, please open an issue at its [feedstock](https://github.com/conda-forge/osmnx-feedstock/issues).
12
+
13
+ ## If you have a feature proposal
14
+
15
+ The OSMnx project follows three principles when considering new functionality: 1) it is useful for a broad set of users, 2) it generalizes well, and 3) it is not trivially easy for users to implement themselves.
16
+
17
+ - Post your proposal on the [issue tracker](https://github.com/gboeing/osmnx/issues), and _provide all the information requested in the template_, so we can review it together (some proposals may not be a good fit for the project).
18
+ - Fork the repo, make your change, update the [changelog](./CHANGELOG.md), run the [tests](./tests), and submit a pull request.
19
+ - Adhere to the project's code and docstring standards by running its [pre-commit](.pre-commit-config.yaml) hooks.
20
+ - Respond to code review.
21
+
22
+ ## If you found a bug
23
+
24
+ - Read the error message, then review the [documentation](https://osmnx.readthedocs.io/) and OSMnx [Examples Gallery](https://github.com/gboeing/osmnx-examples), which cover key concepts, installation, and package usage.
25
+ - Search through the open and closed [issues](https://github.com/gboeing/osmnx/issues) to see if the problem has already been reported.
26
+ - If the problem is with a dependency of OSMnx, open an issue in that dependency's repo.
27
+ - If the problem is with OSMnx itself and you can fix it simply, please open a pull request.
28
+ - If the problem persists, please open an issue in the [issue tracker](https://github.com/gboeing/osmnx/issues), and _provide all the information requested in the template_, including a minimal standalone example so others can independently and completely reproduce the bug.
29
+
30
+ ## Creating a minimal standalone reproducible example
31
+
32
+ We need a minimal standalone example code snippet to be able to reproduce and in turn troubleshoot your problem (also provide the resulting complete error traceback if your code generates an error). This code snippet must be:
33
+
34
+ _Minimal_: the absolute fewest lines of code necessary to reproduce your problem without any extraneous code or data unrelated to your specific problem. This usually requires reworking your code and data down to just a few lines necessary to generate the error.
35
+
36
+ _Standalone_: all imports, data, and variables must be completely defined within the snippet itself so others can independently run it from top to bottom by copying/pasting it into a Python interpreter. Do not link to or load external files and do not provide screenshots of code or error messages: provide all code, data, and tracebacks inline as text.
37
+
38
+ If you're unsure how to create a good reproducible example, read
39
+ [this guide](https://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports).
osmnx/source/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016-2025 Geoff Boeing https://geoffboeing.com/
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
osmnx/source/README.md ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # OSMnx
2
+
3
+ [![PyPI Version](https://badge.fury.io/py/osmnx.svg)](https://pypi.org/project/osmnx/)
4
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/osmnx?period=total&units=international_system&left_color=grey&right_color=brightgreen&left_text=downloads)](https://pepy.tech/project/osmnx)
5
+ [![Documentation Status](https://readthedocs.org/projects/osmnx/badge/?version=latest)](https://osmnx.readthedocs.io/)
6
+ [![Build Status](https://github.com/gboeing/osmnx/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/gboeing/osmnx/actions/workflows/ci.yml)
7
+ [![Coverage Status](https://codecov.io/gh/gboeing/osmnx/branch/main/graph/badge.svg)](https://codecov.io/gh/gboeing/osmnx)
8
+
9
+ **OSMnx** is a Python package to easily download, model, analyze, and visualize street networks and other geospatial features from OpenStreetMap. You can download and model walking, driving, or biking networks with a single line of code then analyze and visualize them. You can just as easily work with urban amenities/points of interest, building footprints, transit stops, elevation data, street orientations, speed/travel time, and routing.
10
+
11
+ ## Citation
12
+
13
+ If you use OSMnx in your work, please cite the paper:
14
+
15
+ Boeing, G. (2025). [Modeling and Analyzing Urban Networks and Amenities with OSMnx](https://doi.org/10.1111/gean.70009). *Geographical Analysis* 57 (4), 567-577. doi:10.1111/gean.70009
16
+
17
+ ## Getting Started
18
+
19
+ First read the [Getting Started](https://osmnx.readthedocs.io/en/stable/getting-started.html) guide for an introduction to the package and FAQ.
20
+
21
+ Then work through the [Examples Gallery](https://github.com/gboeing/osmnx-examples) for step-by-step tutorials and sample code.
22
+
23
+ ## Installation
24
+
25
+ Follow the [Installation](https://osmnx.readthedocs.io/en/stable/installation.html) guide to install OSMnx.
26
+
27
+ ## Support
28
+
29
+ If you have any trouble, consult the [User Reference](https://osmnx.readthedocs.io/en/stable/user-reference.html). The OSMnx repository is hosted on [GitHub](https://github.com/gboeing/osmnx). If you have a "how-to" or usage question, please ask it on [StackOverflow](https://stackoverflow.com/search?q=osmnx), as we reserve the repository's issue tracker for bug tracking and feature development.
30
+
31
+ ## License
32
+
33
+ OSMnx is open source and licensed under the MIT license. OpenStreetMap's open data [license](https://www.openstreetmap.org/copyright/) requires that derivative works provide proper attribution. Refer to the [Getting Started](https://osmnx.readthedocs.io/en/stable/getting-started.html) guide for usage limitations.
osmnx/source/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ osmnx Project Package Initialization File
4
+ """
osmnx/source/docs/.readthedocs.yaml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: 2
2
+
3
+ build:
4
+ os: ubuntu-lts-latest
5
+ tools:
6
+ python: '3'
7
+
8
+ formats: all
9
+
10
+ python:
11
+ install:
12
+ - requirements: ./docs/requirements-docs.txt
13
+
14
+ sphinx:
15
+ configuration: ./docs/source/conf.py
16
+ fail_on_warning: true
osmnx/source/docs/Makefile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Minimal makefile for Sphinx documentation
2
+ #
3
+
4
+ # You can set these variables from the command line, and also
5
+ # from the environment for the first two.
6
+ SPHINXOPTS ?=
7
+ SPHINXBUILD ?= sphinx-build
8
+ SOURCEDIR = source
9
+ BUILDDIR = build
10
+
11
+ # Put it first so that "make" without argument is like "make help".
12
+ help:
13
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14
+
15
+ .PHONY: help Makefile
16
+
17
+ # Catch-all target: route all unknown targets to Sphinx using the new
18
+ # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19
+ %: Makefile
20
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
osmnx/source/docs/make.bat ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @ECHO OFF
2
+
3
+ pushd %~dp0
4
+
5
+ REM Command file for Sphinx documentation
6
+
7
+ if "%SPHINXBUILD%" == "" (
8
+ set SPHINXBUILD=sphinx-build
9
+ )
10
+ set SOURCEDIR=source
11
+ set BUILDDIR=build
12
+
13
+ %SPHINXBUILD% >NUL 2>NUL
14
+ if errorlevel 9009 (
15
+ echo.
16
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
17
+ echo.installed, then set the SPHINXBUILD environment variable to point
18
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
19
+ echo.may add the Sphinx directory to PATH.
20
+ echo.
21
+ echo.If you don't have Sphinx installed, grab it from
22
+ echo.https://www.sphinx-doc.org/
23
+ exit /b 1
24
+ )
25
+
26
+ if "%1" == "" goto help
27
+
28
+ %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29
+ goto end
30
+
31
+ :help
32
+ %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33
+
34
+ :end
35
+ popd
osmnx/source/docs/requirements-docs.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ furo
2
+ sphinx>=7
3
+ sphinx-autodoc-typehints
4
+ -e .
osmnx/source/docs/source/conf.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """
3
+ Configuration file for the Sphinx documentation builder.
4
+
5
+ For the full list of built-in configuration values, see the documentation:
6
+ https://www.sphinx-doc.org/en/master/usage/configuration.html
7
+ """
8
+
9
+ import sys
10
+ from pathlib import Path
11
+ from tomllib import load as toml_load
12
+
13
+ # project info
14
+ author = "Geoff Boeing"
15
+ copyright = "2016-2025, Geoff Boeing" # noqa: A001
16
+ project = "OSMnx"
17
+
18
+ # go up two levels from current working dir (/docs/source) to package root
19
+ pkg_root_path = str(Path.cwd().parent.parent)
20
+ sys.path.insert(0, pkg_root_path)
21
+
22
+ # dynamically load version
23
+ with Path("../../pyproject.toml").open("rb") as f:
24
+ pyproject = toml_load(f)
25
+ version = release = pyproject["project"]["version"]
26
+
27
+ # mock import all required + optional dependency packages because readthedocs
28
+ # does not have them installed
29
+ autodoc_mock_imports = [
30
+ "geopandas",
31
+ "matplotlib",
32
+ "networkx",
33
+ "numpy",
34
+ "pandas",
35
+ "rasterio",
36
+ "requests",
37
+ "rio-vrt",
38
+ "scipy",
39
+ "shapely",
40
+ "sklearn",
41
+ ]
42
+
43
+ # linkcheck for some DOI redirects gets HTTP 403 in CI environment
44
+ linkcheck_ignore = [r"https://doi\.org/.*"]
45
+
46
+ # type annotations configuration
47
+ autodoc_typehints = "description"
48
+ napoleon_use_param = True
49
+ napoleon_use_rtype = False
50
+ typehints_document_rtype = True
51
+ typehints_use_rtype = False
52
+ typehints_fully_qualified = False
53
+
54
+ # general configuration and options for HTML output
55
+ # see https://www.sphinx-doc.org/en/master/usage/configuration.html
56
+ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
57
+ extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx_autodoc_typehints"]
58
+ html_static_path: list[str] = []
59
+ html_theme = "furo"
60
+ language = "en"
61
+ needs_sphinx = "7" # match version from pyproject.toml optional-dependencies
62
+ root_doc = "index"
63
+ source_suffix = ".rst"
64
+ templates_path: list[str] = []
osmnx/source/docs/source/further-reading.rst ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Further Reading
2
+ ===============
3
+
4
+ Boeing, G. (2025). `Modeling and Analyzing Urban Networks and Amenities with OSMnx`_. *Geographical Analysis* 57 (4), 567-577. doi:10.1111/gean.70009
5
+
6
+ This is the official reference paper and citation for the OSMnx package.
7
+
8
+ .. _Modeling and Analyzing Urban Networks and Amenities with OSMnx: https://doi.org/10.1111/gean.70009
9
+
10
+ ----
11
+
12
+ Boeing, G. (2025). `Topological Graph Simplification Solutions to the Street Intersection Miscount Problem`_. *Transactions in GIS* 29 (3), e70037. doi:10.1111/tgis.70037
13
+
14
+ This paper describes and validates the algorithms implemented in OSMnx's :code:`simplification` module and explains why graph simplification is necessary to accurately measure intersection density, street segment length, node degree, etc.
15
+
16
+ .. _Topological Graph Simplification Solutions to the Street Intersection Miscount Problem: https://doi.org/10.1111/tgis.70037
17
+
18
+ ----
19
+
20
+ Boeing, G. (2021). `Street Network Models and Indicators for Every Urban Area in the World`_. *Geographical Analysis* 54 (3), 519-535. doi:10.1111/gean.12281
21
+
22
+ This study uses OSMnx to model and analyze the street networks of every urban area in the world: over 160 million OpenStreetMap street network nodes and over 320 million edges across 8,914 urban areas in 178 countries.
23
+
24
+ .. _Street Network Models and Indicators for Every Urban Area in the World: https://geoffboeing.com/publications/street-network-models-indicators-world/
25
+
26
+ ----
27
+
28
+ Boeing, G. (2020). `The Right Tools for the Job: The Case for Spatial Science Tool-Building`_. *Transactions in GIS* 24 (5), 1299-1314. doi:10.1111/tgis.12678
29
+
30
+ This paper was presented as the 8th annual Transactions in GIS plenary address at the American Association of Geographers annual meeting in Washington, DC. It describes the early development of OSMnx and reviews its use in scientific research over the previous few years.
31
+
32
+ .. _The Right Tools for the Job\: The Case for Spatial Science Tool-Building: https://geoffboeing.com/publications/right-tools-for-job/
33
+
34
+ ----
35
+
36
+ Boeing, G. (2020). `Planarity and Street Network Representation in Urban Form Analysis`_. *Environment and Planning B: Urban Analytics and City Science* 47 (5), 855-869. doi:10.1177/2399808318802941
37
+
38
+ This paper demonstrates the need for nonplanar graphs when modeling urban street networks, which was one of the original motivations for developing OSMnx.
39
+
40
+ .. _Planarity and Street Network Representation in Urban Form Analysis: https://geoffboeing.com/publications/planarity-street-network-representation/
osmnx/source/docs/source/getting-started.rst ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Getting Started
2
+ ===============
3
+
4
+ Get Started in 4 Steps
5
+ ----------------------
6
+
7
+ 1. Install OSMnx by following the :doc:`installation` guide.
8
+
9
+ 2. Read the :ref:`introducing-osmnx` section on this page.
10
+
11
+ 3. Work through the OSMnx `Examples Gallery`_ for step-by-step tutorials and sample code.
12
+
13
+ 4. Consult the :doc:`user-reference` for complete details on using the package.
14
+
15
+ Finally, if you're not already familiar with `NetworkX`_ and `GeoPandas`_, make sure you read their user guides as OSMnx uses their data structures.
16
+
17
+ .. _introducing-osmnx:
18
+
19
+ Introducing OSMnx
20
+ -----------------
21
+
22
+ This quick introduction explains key concepts and the basic functionality of OSMnx.
23
+
24
+ Overview
25
+ ^^^^^^^^
26
+
27
+ OSMnx is pronounced as the initialism: "oh-ess-em-en-ex". It is built on top of NetworkX and GeoPandas, and interacts with `OpenStreetMap`_ APIs to:
28
+
29
+ * Download and model street networks or other infrastructure anywhere in the world with a single line of code
30
+ * Download geospatial features (e.g., political boundaries, building footprints, grocery stores, transit stops) as a GeoDataFrame
31
+ * Query by city name, polygon, bounding box, or point/address + distance
32
+ * Model driving, walking, biking, and other travel modes
33
+ * Attach node elevations from a local raster file or web service and calculate edge grades
34
+ * Impute missing speeds and calculate graph edge travel times
35
+ * Simplify and correct the network's topology to clean-up nodes and consolidate complex intersections
36
+ * Fast map-matching of points, routes, or trajectories to nearest graph edges or nodes
37
+ * Save/load network to/from disk as GraphML, GeoPackage, or OSM XML file
38
+ * Conduct topological and spatial analyses to automatically calculate dozens of indicators
39
+ * Calculate and visualize street bearings and orientations
40
+ * Calculate and visualize shortest-path routes that minimize distance, travel time, elevation, etc
41
+ * Explore street networks and geospatial features as a static map or interactive web map
42
+ * Visualize travel distance and travel time with isoline and isochrone maps
43
+ * Plot figure-ground diagrams of street networks and building footprints
44
+
45
+ The OSMnx `Examples Gallery`_ contains tutorials and demonstrations of all these features, and package usage is detailed in the :doc:`user-reference`.
46
+
47
+ Configuration
48
+ ^^^^^^^^^^^^^
49
+
50
+ You can configure OSMnx using the ``settings`` module. Here you can adjust logging behavior, caching, server endpoints, and more. You can also configure OSMnx to retrieve historical snapshots of OpenStreetMap data as of a certain date.
51
+
52
+ Read more about the :ref:`settings <osmnx-settings-module>` module in the User Reference.
53
+
54
+ Geocoding and Querying
55
+ ^^^^^^^^^^^^^^^^^^^^^^
56
+
57
+ OSMnx geocodes place names and addresses with the OpenStreetMap `Nominatim`_ API. You can use the ``geocoder`` module to geocode place names or addresses to lat-lon coordinates. Or, you can retrieve place boundaries or any other OpenStreetMap elements by name or ID. Read more about the :ref:`geocoder <osmnx-geocoder-module>` module in the User Reference.
58
+
59
+ Using the ``features`` and ``graph`` modules, as described below, you can download data by lat-lon point, address, bounding box, bounding polygon, or place name (e.g., neighborhood, city, county, etc).
60
+
61
+ Urban Amenities
62
+ ^^^^^^^^^^^^^^^
63
+
64
+ Using OSMnx's ``features`` module, you can search for and download any geospatial `features`_ (such as building footprints, grocery stores, schools, public parks, transit stops, etc) from the OpenStreetMap `Overpass`_ API as a GeoPandas GeoDataFrame. This uses OpenStreetMap `tags`_ to search for matching `elements`_.
65
+
66
+ Read more about the :ref:`features <osmnx-features-module>` module in the User Reference.
67
+
68
+ Modeling a Network
69
+ ^^^^^^^^^^^^^^^^^^
70
+
71
+ Using OSMnx's ``graph`` module, you can retrieve any spatial network data (such as streets, paths, rail, canals, etc) from the Overpass API and model them as NetworkX `MultiDiGraphs`_.
72
+
73
+ In short, MultiDiGraphs are nonplanar directed graphs with possible self-loops and parallel edges. Thus, a one-way street will be represented with a single directed edge from node *u* to node *v*, but a bidirectional street will be represented with two reciprocal directed edges (with identical geometries): one from node *u* to node *v* and another from *v* to *u*, to represent both possible directions of flow. Because these graphs are nonplanar, they correctly model the topology of interchanges, bridges, and tunnels. That is, edge crossings in a two-dimensional plane are not intersections in an OSMnx model unless they represent true junctions in the three-dimensional real world.
74
+
75
+ The ``graph`` module uses filters to query the Overpass API: you can either specify a built-in network type or provide your own custom filter with `Overpass QL`_. Under the hood, OSMnx does several things to generate the best possible model. It initially creates a 500m-buffered graph before truncating it to your desired query area, to ensure accurate streets-per-node stats and to attenuate graph perimeter effects. By default, it returns the largest weakly connected component. It also simplifies the graph topology as discussed below.
76
+
77
+ Read more about the :ref:`graph <osmnx-graph-module>` module in the User Reference and refer to the official reference paper at the :doc:`further-reading` page for complete modeling details.
78
+
79
+ Topology Clean-Up
80
+ ^^^^^^^^^^^^^^^^^
81
+
82
+ The ``simplification`` module automatically processes the network's topology from the original raw OpenStreetMap data, such that nodes represent intersections/dead-ends and edges represent the street segments that link them. This takes two primary forms: graph simplification and intersection consolidation.
83
+
84
+ **Graph simplification** cleans up the graph's topology so that nodes represent intersections or dead-ends and edges represent street segments. This is important because in OpenStreetMap raw data, ways comprise sets of straight-line segments between nodes: that is, nodes are vertices for streets' curving line geometries, not just intersections and dead-ends. By default, OSMnx simplifies this topology by discarding non-intersection/dead-end nodes while retaining the complete true edge geometry as an edge attribute. When multiple OpenStreetMap ways are merged into a single graph edge, the ways' attribute values can be aggregated into a single value.
85
+
86
+ **Intersection consolidation** is important because many real-world street networks feature complex intersections and traffic circles, resulting in a cluster of graph nodes where there is really just one true intersection as we would think of it in transportation or urban design. Similarly, divided roads are often represented by separate centerline edges: the intersection of two divided roads thus creates 4 nodes, representing where each edge intersects a perpendicular edge, but these 4 nodes represent a single intersection in the real world. OSMnx can consolidate such complex intersections into a single node and optionally rebuild the graph's edge topology accordingly. When multiple OpenStreetMap nodes are merged into a single graph node, the nodes' attribute values can be aggregated into a single value.
87
+
88
+ Read more about the :ref:`simplification <osmnx-simplification-module>` module in the User Reference.
89
+
90
+ Model Attributes
91
+ ^^^^^^^^^^^^^^^^
92
+
93
+ An OSMnx model has some standard required attributes, plus some optional attributes. The latter are sometimes present based on the source OSM data's tagging, the ``settings`` module configuration, and any processing you may have done to add additional attributes (as noted in various functions' documentation).
94
+
95
+ As a NetworkX `MultiDiGraph`_ object, it has top-level ``graph``, ``nodes``, and ``edges`` attributes. The ``graph`` attribute dictionary must contain a "crs" key defining its coordinate reference system. The ``nodes`` are identified by OSM ID and each must contain a ``data`` attribute dictionary that must have "x" and "y" keys defining its coordinates and a "street_count" key defining how many physical streets are incident to it. The ``edges`` are identified by a 3-tuple of "u" (source node ID), "v" (target node ID), and "key" (to differentiate parallel edges), and each must contain a ``data`` attribute dictionary that must have an "osmid" key defining its OSM ID and a "length" key defining its length in meters.
96
+
97
+ The OSMnx ``graph`` module automatically creates MultiDiGraphs with these required attributes, plus additional optional attributes based on the ``settings`` module configuration. If you instead manually create your own graph model, make sure it has these required attributes at a minimum.
98
+
99
+ Convert, Project, Save
100
+ ^^^^^^^^^^^^^^^^^^^^^^
101
+
102
+ OSMnx's ``convert`` module can convert a MultiDiGraph to a `DiGraph`_ if you prefer a directed representation of the network without any parallel edges, or to a `MultiGraph`_ if you need an undirected representation for use with functions or algorithms that only accept a MultiGraph object. If you just want a fully bidirectional graph (such as for a walking network), just configure the ``settings`` module's ``bidirectional_network_types`` before creating your graph.
103
+
104
+ The ``convert`` module can also convert a MultiDiGraph to/from GeoPandas node and edge `GeoDataFrames`_. The nodes GeoDataFrame is indexed by OSM ID and the edges GeoDataFrame is multi-indexed by ``u, v, key`` just like a NetworkX edge. This allows you to load arbitrary node/edge ShapeFiles or GeoPackage layers as GeoDataFrames then model them as a MultiDiGraph for graph analysis. The ``convert`` module exposes validation functions to verify that your MultiDiGraph or GeoDataFrames satisfy OSMnx requirements. Read more about the :ref:`convert <osmnx-convert-module>` module in the User Reference.
105
+
106
+ You can easily project your graph to different coordinate reference systems using the ``projection`` module. If you're unsure which `CRS`_ you want to project to, OSMnx can automatically determine an appropriate UTM CRS for you. Read more about the :ref:`projection <osmnx-projection-module>` module in the User Reference.
107
+
108
+ Using the ``io`` module, you can save your graph to disk as a GraphML file (to load into other network analysis software), a GeoPackage (to load into other GIS software), or an OSM XML file. Use the GraphML format whenever saving a graph for later work with OSMnx. Read more about the :ref:`io <osmnx-io-module>` module in the User Reference.
109
+
110
+ Network Measures
111
+ ^^^^^^^^^^^^^^^^
112
+
113
+ You can use the ``stats`` module to calculate a variety of geometric and topological measures as well as street network bearing and orientation statistics. These measures define streets as the edges in an undirected representation of the graph to prevent double-counting bidirectional edges of a two-way street. You can easily generate common stats in transportation studies, urban design, and network science, including intersection density, circuity, average node degree (connectedness), betweenness centrality, and much more. Read more about the :ref:`stats <osmnx-stats-module>` module in the User Reference.
114
+
115
+ You can also use NetworkX directly to calculate additional topological network measures.
116
+
117
+ Working with Elevation
118
+ ^^^^^^^^^^^^^^^^^^^^^^
119
+
120
+ The ``elevation`` module lets you automatically attach elevations to the graph's nodes from a local raster file or the Google Maps `Elevation API`_ (or equivalent web API with a compatible interface). You can also calculate edge grades (i.e., rise-over-run) and analyze the steepness of certain streets or routes.
121
+
122
+ Read more about the :ref:`elevation <osmnx-elevation-module>` module in the User Reference.
123
+
124
+ Routing
125
+ ^^^^^^^
126
+
127
+ The ``distance`` module can find the nearest node(s) or edge(s) to coordinates using a fast spatial index. The ``routing`` module can solve shortest paths for network routing, parallelized with multiprocessing, using different weights (e.g., distance, travel time, elevation change, etc). It can also impute missing speeds to the graph edges. This imputation can obviously be imprecise, so the user can override it by passing in arguments that define local speed limits. It can also calculate free-flow travel times for each edge.
128
+
129
+ Read more about the :ref:`distance <osmnx-distance-module>` and :ref:`routing <osmnx-routing-module>` modules in the User Reference.
130
+
131
+ Visualization
132
+ ^^^^^^^^^^^^^
133
+
134
+ You can plot graphs, routes, network figure-ground diagrams, building footprints, and street network orientation rose diagrams (aka, polar histograms) with the ``plot`` module. You can also explore street networks, routes, or geospatial features as interactive `Folium`_ web maps.
135
+
136
+ Read more about the :ref:`plot <osmnx-plot-module>` module in the User Reference.
137
+
138
+ Usage Limits
139
+ ^^^^^^^^^^^^
140
+
141
+ Refer to the `Nominatim Usage Policy`_ and `Overpass Commons`_ documentation for API usage limits and restrictions to which you must adhere. If you configure OSMnx to use an alternative API instance, ensure you understand and follow their policies. If you feel you need to exceed these limits, consider installing your own hosted instance and setting OSMnx to use it.
142
+
143
+ More Info
144
+ ---------
145
+
146
+ All of this functionality is demonstrated step-by-step in the OSMnx `Examples Gallery`_, and usage is detailed in the :doc:`user-reference`. Feature development details are in the `Changelog`_. Consult the :doc:`further-reading` resources for additional technical details and research.
147
+
148
+ Frequently Asked Questions
149
+ --------------------------
150
+
151
+ *How do I install OSMnx?* Follow the :doc:`installation` guide.
152
+
153
+ *How do I use OSMnx?* Check out the step-by-step tutorials in the OSMnx `Examples Gallery`_.
154
+
155
+ *How does this or that function work?* Consult the :doc:`user-reference`.
156
+
157
+ *What can I do with OSMnx?* Check out recent `projects`_ that use OSMnx.
158
+
159
+ *I have a usage question.* Please ask it on `StackOverflow`_.
160
+
161
+
162
+ .. _Changelog: https://github.com/gboeing/osmnx/blob/main/CHANGELOG.md
163
+ .. _CRS: https://en.wikipedia.org/wiki/Coordinate_reference_system
164
+ .. _DiGraph: https://networkx.org/documentation/stable/reference/classes/digraph.html
165
+ .. _elements: https://wiki.openstreetmap.org/wiki/Elements
166
+ .. _Elevation API: https://developers.google.com/maps/documentation/elevation
167
+ .. _Examples Gallery: https://github.com/gboeing/osmnx-examples
168
+ .. _features: https://wiki.openstreetmap.org/wiki/Map_features
169
+ .. _Folium: https://python-visualization.github.io/folium/
170
+ .. _GeoDataFrames: https://geopandas.org/en/stable/docs/reference/geodataframe.html
171
+ .. _GeoPandas: https://geopandas.org
172
+ .. _MultiDiGraph: https://networkx.org/documentation/stable/reference/classes/multidigraph.html
173
+ .. _MultiDiGraphs: https://networkx.org/documentation/stable/reference/classes/multidigraph.html
174
+ .. _MultiGraph: https://networkx.org/documentation/stable/reference/classes/multigraph.html
175
+ .. _NetworkX: https://networkx.org
176
+ .. _Nominatim: https://nominatim.org
177
+ .. _Nominatim Usage Policy: https://operations.osmfoundation.org/policies/nominatim/
178
+ .. _OpenStreetMap: https://www.openstreetmap.org
179
+ .. _Overpass: https://wiki.openstreetmap.org/wiki/Overpass_API
180
+ .. _Overpass Commons: https://dev.overpass-api.de/overpass-doc/en/preface/commons.html
181
+ .. _Overpass QL: https://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_QL
182
+ .. _projects: https://geoffboeing.com/2018/03/osmnx-features-roundup
183
+ .. _StackOverflow: https://stackoverflow.com/search?q=osmnx
184
+ .. _tags: https://wiki.openstreetmap.org/wiki/Tags
osmnx/source/docs/source/index.rst ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ OSMnx |version|
2
+ ===============
3
+
4
+ **OSMnx** is a Python package to easily download, model, analyze, and visualize street networks and other geospatial features from OpenStreetMap. You can download and model walking, driving, or biking networks with a single line of code then analyze and visualize them. You can just as easily work with urban amenities/points of interest, building footprints, transit stops, elevation data, street orientations, speed/travel time, and routing.
5
+
6
+ .. _migration guide: https://github.com/gboeing/osmnx/issues/1123
7
+
8
+ Citation
9
+ --------
10
+
11
+ If you use OSMnx in your work, please cite the paper:
12
+
13
+ Boeing, G. (2025). `Modeling and Analyzing Urban Networks and Amenities with OSMnx`_. *Geographical Analysis* 57 (4), 567-577. doi:10.1111/gean.70009
14
+
15
+ .. _Modeling and Analyzing Urban Networks and Amenities with OSMnx: https://doi.org/10.1111/gean.70009
16
+
17
+
18
+ Getting Started
19
+ ---------------
20
+
21
+ First read the :doc:`getting-started` guide for an introduction to the package and FAQ.
22
+
23
+ Then work through the `Examples Gallery`_ for step-by-step tutorials and sample code.
24
+
25
+ .. _Examples Gallery: https://github.com/gboeing/osmnx-examples
26
+
27
+
28
+ Installation
29
+ ------------
30
+
31
+ Follow the :doc:`installation` guide to install OSMnx.
32
+
33
+
34
+ Support
35
+ -------
36
+ If you have any trouble, consult the :doc:`user-reference`. The OSMnx repository is hosted on `GitHub`_. If you have a "how-to" or usage question, please ask it on `StackOverflow`_, as we reserve the repository's issue tracker for bug tracking and feature development.
37
+
38
+ .. _GitHub: https://github.com/gboeing/osmnx
39
+ .. _StackOverflow: https://stackoverflow.com/search?q=osmnx
40
+
41
+
42
+ License
43
+ -------
44
+
45
+ OSMnx is open source and licensed under the MIT license. OpenStreetMap's open data `license`_ requires that derivative works provide proper attribution. Refer to the :doc:`getting-started` guide for usage limitations.
46
+
47
+ .. _license: https://www.openstreetmap.org/copyright
48
+
49
+
50
+ User Guides
51
+ -----------
52
+
53
+ .. toctree::
54
+ :maxdepth: 1
55
+
56
+ installation
57
+
58
+ .. toctree::
59
+ :maxdepth: 1
60
+
61
+ getting-started
62
+
63
+ .. toctree::
64
+ :maxdepth: 1
65
+
66
+ user-reference
67
+
68
+ .. toctree::
69
+ :maxdepth: 1
70
+
71
+ internals-reference
72
+
73
+ .. toctree::
74
+ :maxdepth: 1
75
+
76
+ further-reading
77
+
78
+
79
+ Indices
80
+ -------
81
+
82
+ * :ref:`genindex`
83
+ * :ref:`modindex`
84
+ * :ref:`search`
osmnx/source/docs/source/installation.rst ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Installation
2
+ ============
3
+
4
+ Conda
5
+ -----
6
+
7
+ The foolproof way to install OSMnx is with `conda`_ or `mamba`_:
8
+
9
+ .. code-block:: shell
10
+
11
+ conda create --strict-channel-priority -c conda-forge -n ox osmnx
12
+
13
+ This creates a new conda environment and installs OSMnx into it, via the conda-forge channel. If you want other packages, such as :code:`jupyterlab`, installed in this environment as well, just add their names after :code:`osmnx` above. To upgrade OSMnx to a newer release, remove the conda environment you created and then create a new one again, as above. See the `conda-forge`_ documentation for details.
14
+
15
+ Docker
16
+ ------
17
+
18
+ You can run OSMnx + JupyterLab directly from the official OSMnx `Docker`_ image.
19
+
20
+ Pip
21
+ ---
22
+
23
+ You can also install OSMnx with `uv`_ or `pip`_ (into a virtual environment):
24
+
25
+ .. code-block:: shell
26
+
27
+ pip install osmnx
28
+
29
+ OSMnx is written in pure Python and distributed on `PyPI`_. Its installation alone is thus trivially simple if you have its dependencies installed and tested on your system. However, OSMnx depends on other packages that in turn depend on compiled C/C++ libraries, which may present some challenges depending on your specific system's configuration. If precompiled binaries are not available for your system, you may need to compile and configure those dependencies by following their installation instructions. So, if you're not sure what you're doing, just follow the conda instructions above to avoid installation problems.
30
+
31
+ .. _conda: https://conda.io/
32
+ .. _conda-forge: https://conda-forge.org/
33
+ .. _Docker: https://hub.docker.com/r/gboeing/osmnx
34
+ .. _mamba: https://mamba.readthedocs.io/
35
+ .. _pip: https://pip.pypa.io/
36
+ .. _PyPI: https://pypi.org/project/osmnx/
37
+ .. _uv: https://docs.astral.sh/uv/
osmnx/source/docs/source/internals-reference.rst ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Internals Reference
2
+ ===================
3
+
4
+ This is the complete OSMnx internals reference for developers, including private internal modules and functions. If you are instead looking for a user guide to OSMnx's public API, see the :doc:`user-reference`.
5
+
6
+ osmnx._api_v1 module
7
+ --------------------
8
+
9
+ .. automodule:: osmnx._api_v1
10
+ :members:
11
+ :private-members:
12
+ :noindex:
13
+
14
+ osmnx.bearing module
15
+ --------------------
16
+
17
+ .. automodule:: osmnx.bearing
18
+ :members:
19
+ :private-members:
20
+ :noindex:
21
+
22
+ osmnx.convert module
23
+ ---------------------
24
+
25
+ .. automodule:: osmnx.convert
26
+ :members:
27
+ :private-members:
28
+ :noindex:
29
+
30
+ osmnx.distance module
31
+ ---------------------
32
+
33
+ .. automodule:: osmnx.distance
34
+ :members:
35
+ :private-members:
36
+ :noindex:
37
+
38
+ osmnx.elevation module
39
+ ----------------------
40
+
41
+ .. automodule:: osmnx.elevation
42
+ :members:
43
+ :private-members:
44
+ :noindex:
45
+
46
+ osmnx._errors module
47
+ --------------------
48
+
49
+ .. automodule:: osmnx._errors
50
+ :members:
51
+ :private-members:
52
+ :noindex:
53
+
54
+ osmnx.features module
55
+ ---------------------
56
+
57
+ .. automodule:: osmnx.features
58
+ :members:
59
+ :private-members:
60
+ :noindex:
61
+
62
+ osmnx.geocoder module
63
+ ---------------------
64
+
65
+ .. automodule:: osmnx.geocoder
66
+ :members:
67
+ :private-members:
68
+ :noindex:
69
+
70
+ osmnx.graph module
71
+ ------------------
72
+
73
+ .. automodule:: osmnx.graph
74
+ :members:
75
+ :private-members:
76
+ :noindex:
77
+
78
+ osmnx._http module
79
+ ------------------
80
+
81
+ .. automodule:: osmnx._http
82
+ :members:
83
+ :private-members:
84
+ :noindex:
85
+
86
+ osmnx.io module
87
+ ---------------
88
+
89
+ .. automodule:: osmnx.io
90
+ :members:
91
+ :private-members:
92
+ :noindex:
93
+
94
+ osmnx._nominatim module
95
+ -----------------------
96
+
97
+ .. automodule:: osmnx._nominatim
98
+ :members:
99
+ :private-members:
100
+ :noindex:
101
+
102
+ osmnx._osm_xml module
103
+ ---------------------
104
+
105
+ .. automodule:: osmnx._osm_xml
106
+ :members:
107
+ :private-members:
108
+ :noindex:
109
+
110
+ osmnx._overpass module
111
+ ----------------------
112
+
113
+ .. automodule:: osmnx._overpass
114
+ :members:
115
+ :private-members:
116
+ :noindex:
117
+
118
+ osmnx.plot module
119
+ -----------------
120
+
121
+ .. automodule:: osmnx.plot
122
+ :members:
123
+ :private-members:
124
+ :noindex:
125
+
126
+ osmnx.projection module
127
+ -----------------------
128
+
129
+ .. automodule:: osmnx.projection
130
+ :members:
131
+ :private-members:
132
+ :noindex:
133
+
134
+ osmnx.routing module
135
+ -----------------------
136
+
137
+ .. automodule:: osmnx.routing
138
+ :members:
139
+ :private-members:
140
+ :noindex:
141
+
142
+ osmnx.settings module
143
+ ---------------------
144
+
145
+ .. automodule:: osmnx.settings
146
+ :members:
147
+ :private-members:
148
+ :noindex:
149
+
150
+ osmnx.simplification module
151
+ ---------------------------
152
+
153
+ .. automodule:: osmnx.simplification
154
+ :members:
155
+ :private-members:
156
+ :noindex:
157
+
158
+ osmnx.stats module
159
+ ------------------
160
+
161
+ .. automodule:: osmnx.stats
162
+ :members:
163
+ :private-members:
164
+ :noindex:
165
+
166
+ osmnx.truncate module
167
+ ---------------------
168
+
169
+ .. automodule:: osmnx.truncate
170
+ :members:
171
+ :private-members:
172
+ :noindex:
173
+
174
+ osmnx.utils module
175
+ ------------------
176
+
177
+ .. automodule:: osmnx.utils
178
+ :members:
179
+ :private-members:
180
+ :noindex:
181
+
182
+ osmnx.utils_geo module
183
+ ----------------------
184
+
185
+ .. automodule:: osmnx.utils_geo
186
+ :members:
187
+ :private-members:
188
+ :noindex:
189
+
190
+ osmnx._validate module
191
+ ----------------------
192
+
193
+ .. automodule:: osmnx._validate
194
+ :members:
195
+ :private-members:
196
+ :noindex:
osmnx/source/docs/source/user-reference.rst ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ User Reference
2
+ ==============
3
+
4
+ This is the User Reference for the OSMnx package. If you are looking for an introduction to OSMnx, read the :doc:`getting-started` guide. This guide describes the usage of OSMnx's public API.
5
+
6
+ .. _migration guide: https://github.com/gboeing/osmnx/issues/1123
7
+
8
+ .. _osmnx-bearing-module:
9
+
10
+ osmnx.bearing module
11
+ --------------------
12
+
13
+ .. automodule:: osmnx.bearing
14
+ :members:
15
+
16
+ .. _osmnx-convert-module:
17
+
18
+ osmnx.convert module
19
+ --------------------
20
+
21
+ .. automodule:: osmnx.convert
22
+ :members:
23
+
24
+ .. _osmnx-distance-module:
25
+
26
+ osmnx.distance module
27
+ ---------------------
28
+
29
+ .. automodule:: osmnx.distance
30
+ :members:
31
+
32
+ .. _osmnx-elevation-module:
33
+
34
+ osmnx.elevation module
35
+ ----------------------
36
+
37
+ .. automodule:: osmnx.elevation
38
+ :members:
39
+
40
+ .. _osmnx-features-module:
41
+
42
+ osmnx.features module
43
+ ---------------------
44
+
45
+ .. automodule:: osmnx.features
46
+ :members:
47
+
48
+ .. _osmnx-geocoder-module:
49
+
50
+ osmnx.geocoder module
51
+ ---------------------
52
+
53
+ .. automodule:: osmnx.geocoder
54
+ :members:
55
+
56
+ .. _osmnx-graph-module:
57
+
58
+ osmnx.graph module
59
+ ------------------
60
+
61
+ .. automodule:: osmnx.graph
62
+ :members:
63
+
64
+ .. _osmnx-io-module:
65
+
66
+ osmnx.io module
67
+ ---------------
68
+
69
+ .. automodule:: osmnx.io
70
+ :members:
71
+
72
+ .. _osmnx-plot-module:
73
+
74
+ osmnx.plot module
75
+ -----------------
76
+
77
+ .. automodule:: osmnx.plot
78
+ :members:
79
+
80
+ .. _osmnx-projection-module:
81
+
82
+ osmnx.projection module
83
+ -----------------------
84
+
85
+ .. automodule:: osmnx.projection
86
+ :members:
87
+
88
+ .. _osmnx-routing-module:
89
+
90
+ osmnx.routing module
91
+ -----------------------
92
+
93
+ .. automodule:: osmnx.routing
94
+ :members:
95
+
96
+ .. _osmnx-settings-module:
97
+
98
+ osmnx.settings module
99
+ ---------------------
100
+
101
+ .. automodule:: osmnx.settings
102
+ :members:
103
+
104
+ .. _osmnx-simplification-module:
105
+
106
+ osmnx.simplification module
107
+ ---------------------------
108
+
109
+ .. automodule:: osmnx.simplification
110
+ :members:
111
+
112
+ .. _osmnx-stats-module:
113
+
114
+ osmnx.stats module
115
+ ------------------
116
+
117
+ .. automodule:: osmnx.stats
118
+ :members:
119
+
120
+ .. _osmnx-truncate-module:
121
+
122
+ osmnx.truncate module
123
+ ---------------------
124
+
125
+ .. automodule:: osmnx.truncate
126
+ :members:
127
+
128
+ .. _osmnx-utils-module:
129
+
130
+ osmnx.utils module
131
+ ------------------
132
+
133
+ .. automodule:: osmnx.utils
134
+ :members:
135
+
136
+ .. _osmnx-utils_geo-module:
137
+
138
+ osmnx.utils_geo module
139
+ ----------------------
140
+
141
+ .. automodule:: osmnx.utils_geo
142
+ :members:
osmnx/source/environments/create_conda_env.sh ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ echo "Run conda deactivate before running this script."
4
+ ENV=ox
5
+ ENV_PATH=$(conda info --base)/envs/$ENV
6
+ PACKAGE=osmnx
7
+ uv --version
8
+ eval "$(conda shell.bash hook)"
9
+ conda deactivate
10
+ conda env remove --yes -n $ENV || true
11
+ conda create --yes -c conda-forge --strict-channel-priority -n $ENV python
12
+ eval "$(conda shell.bash hook)"
13
+ conda activate $ENV
14
+ uv export --no-build --all-extras --all-groups > ./environments/requirements-temp.txt
15
+ uv pip install --no-build --strict -r ./environments/requirements-temp.txt
16
+ rm -f ./environments/requirements-temp.txt
17
+ python -m pip --python "$ENV_PATH" uninstall $PACKAGE --yes
18
+ python -m pip --python "$ENV_PATH" install -e .
19
+ python -m ipykernel install --prefix "$ENV_PATH" --name $ENV --display-name "Python ($ENV)"
20
+ conda list -n $ENV
21
+ python -m pip --python "$ENV_PATH" check
22
+ jupyter kernelspec list
23
+ ipython -c "import $PACKAGE; print('$PACKAGE version', $PACKAGE.__version__)"
osmnx/source/environments/docker/Dockerfile ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM jupyter/base-notebook
2
+ LABEL maintainer="Geoff Boeing <boeing@usc.edu>"
3
+ LABEL url="https://osmnx.readthedocs.io"
4
+ LABEL description="OSMnx is a Python package to easily download, model, analyze, and visualize street networks and other geospatial features from OpenStreetMap."
5
+
6
+ # expose $TARGETPLATFORM to the install.sh script
7
+ ARG TARGETPLATFORM
8
+
9
+ # copy uv binaries and package files needed for installation
10
+ COPY --from=ghcr.io/astral-sh/uv:0.9 /uv /uvx /bin/
11
+ COPY --chown=jovyan --chmod=0755 ./environments/docker/install.sh ./osmnx/
12
+ COPY --chown=jovyan --chmod=0755 ./osmnx/ ./osmnx/osmnx/
13
+ COPY --chown=jovyan --chmod=0755 ./LICENSE.txt ./osmnx/
14
+ COPY --chown=jovyan --chmod=0755 ./pyproject.toml ./osmnx/
15
+ COPY --chown=jovyan --chmod=0755 ./README.md ./osmnx/
16
+
17
+ # install and configure everything in one RUN to keep image tidy
18
+ RUN cd ./osmnx && bash install.sh
19
+
20
+ # set jupyter working directory to map to mounted volume
21
+ WORKDIR /home/jovyan/work
22
+
23
+ # set default command to launch when container is run
24
+ CMD ["jupyter", "lab", "--ip='0.0.0.0'", "--port=8888", "--no-browser", "--NotebookApp.token=''", "--NotebookApp.password=''"]
osmnx/source/environments/docker/build_image.sh ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ echo "Run this script from the repository root."
4
+ docker login
5
+ docker buildx build --progress=plain --no-cache --pull --push --platform=linux/amd64,linux/arm64 -f ./environments/docker/Dockerfile -t gboeing/osmnx:test .
6
+ IMPORTED_VERSION=$(docker run --rm gboeing/osmnx:test /bin/bash -c "ipython -c \"import osmnx; print(osmnx.__version__)\"")
7
+ echo "Imported $IMPORTED_VERSION"
osmnx/source/environments/docker/install.sh ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # rasterio doesn't provide linux/arm64 wheels, so if the target platform is
5
+ # linux/arm64, don't install this optional dependency (attempting to build
6
+ # it rather than install the wheel will also fail).
7
+ # see https://github.com/rasterio/rasterio-wheels/issues/69
8
+ if [[ "$TARGETPLATFORM" == "linux/arm64" ]]
9
+ then
10
+ NOEXTRA="--no-extra=raster --no-extra=all"
11
+ else
12
+ NOEXTRA=""
13
+ fi
14
+
15
+ # install all requirements into the existing system environment
16
+ uv export --no-cache --no-build --all-extras $NOEXTRA --group examples > requirements-temp.txt
17
+ uv pip install --no-cache --no-build --system --compile-bytecode --strict -r requirements-temp.txt
18
+ rm -f requirements-temp.txt
19
+ uv cache clean
20
+ python --version
21
+ ipython -c "import osmnx; print('OSMnx version', osmnx.__version__)"
osmnx/source/osmnx/__init__.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ruff: noqa: D205 # numpydoc ignore=SS06
2
+ """
3
+ OSMnx is a Python package to easily download, model, analyze, and visualize
4
+ street networks and other geospatial features from OpenStreetMap.
5
+
6
+ Full documentation at: https://osmnx.readthedocs.io
7
+
8
+ If you use OSMnx in your work, please cite: https://doi.org/10.1111/gean.70009
9
+ """
10
+
11
+ from importlib.metadata import version as metadata_version
12
+
13
+ # expose the package version
14
+ __version__ = metadata_version("osmnx")
15
+
16
+ # expose the package's public modules
17
+ from . import _errors as _errors
18
+ from . import bearing as bearing
19
+ from . import convert as convert
20
+ from . import distance as distance
21
+ from . import elevation as elevation
22
+ from . import features as features
23
+ from . import geocoder as geocoder
24
+ from . import graph as graph
25
+ from . import io as io
26
+ from . import plot as plot
27
+ from . import projection as projection
28
+ from . import routing as routing
29
+ from . import settings as settings
30
+ from . import simplification as simplification
31
+ from . import stats as stats
32
+ from . import truncate as truncate
33
+ from . import utils as utils
34
+ from . import utils_geo as utils_geo
35
+
36
+ # expose the old v1 API for backwards compatibility
37
+ from ._api_v1 import * # noqa: F403
osmnx/source/osmnx/_api_v1.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ruff: noqa: PLC0414
2
+ """
3
+ Expose the old v1 API for backwards compatibility.
4
+
5
+ This allows common functionality to be accessed directly via the
6
+ ox.function_name() shortcut by exposing these functions directly in the
7
+ package's namespace.
8
+ """
9
+
10
+ from .bearing import add_edge_bearings as add_edge_bearings
11
+ from .bearing import orientation_entropy as orientation_entropy
12
+ from .convert import graph_from_gdfs as graph_from_gdfs
13
+ from .convert import graph_to_gdfs as graph_to_gdfs
14
+ from .distance import nearest_edges as nearest_edges
15
+ from .distance import nearest_nodes as nearest_nodes
16
+ from .elevation import add_edge_grades as add_edge_grades
17
+ from .elevation import add_node_elevations_google as add_node_elevations_google
18
+ from .elevation import add_node_elevations_raster as add_node_elevations_raster
19
+ from .features import features_from_address as features_from_address
20
+ from .features import features_from_bbox as features_from_bbox
21
+ from .features import features_from_place as features_from_place
22
+ from .features import features_from_point as features_from_point
23
+ from .features import features_from_polygon as features_from_polygon
24
+ from .features import features_from_xml as features_from_xml
25
+ from .geocoder import geocode as geocode
26
+ from .geocoder import geocode_to_gdf as geocode_to_gdf
27
+ from .graph import graph_from_address as graph_from_address
28
+ from .graph import graph_from_bbox as graph_from_bbox
29
+ from .graph import graph_from_place as graph_from_place
30
+ from .graph import graph_from_point as graph_from_point
31
+ from .graph import graph_from_polygon as graph_from_polygon
32
+ from .graph import graph_from_xml as graph_from_xml
33
+ from .io import load_graphml as load_graphml
34
+ from .io import save_graph_geopackage as save_graph_geopackage
35
+ from .io import save_graph_xml as save_graph_xml
36
+ from .io import save_graphml as save_graphml
37
+ from .plot import plot_figure_ground as plot_figure_ground
38
+ from .plot import plot_footprints as plot_footprints
39
+ from .plot import plot_graph as plot_graph
40
+ from .plot import plot_graph_route as plot_graph_route
41
+ from .plot import plot_graph_routes as plot_graph_routes
42
+ from .plot import plot_orientation as plot_orientation
43
+ from .projection import project_graph as project_graph
44
+ from .routing import add_edge_speeds as add_edge_speeds
45
+ from .routing import add_edge_travel_times as add_edge_travel_times
46
+ from .routing import k_shortest_paths as k_shortest_paths
47
+ from .routing import shortest_path as shortest_path
48
+ from .simplification import consolidate_intersections as consolidate_intersections
49
+ from .simplification import simplify_graph as simplify_graph
50
+ from .stats import basic_stats as basic_stats
51
+ from .utils import citation as citation
52
+ from .utils import log as log
53
+ from .utils import ts as ts
osmnx/source/osmnx/_errors.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Define custom errors and exceptions."""
2
+
3
+
4
+ class CacheOnlyInterruptError(InterruptedError):
5
+ """Exception for `settings.cache_only_mode=True` interruption."""
6
+
7
+
8
+ class GraphSimplificationError(ValueError):
9
+ """Exception for a problem with graph simplification."""
10
+
11
+
12
+ class ValidationError(ValueError):
13
+ """Exception for failed graph or node/edge GeoDataFrame validation."""
14
+
15
+
16
+ class InsufficientResponseError(ValueError):
17
+ """Exception for empty or too few results in server response."""
18
+
19
+
20
+ class ResponseStatusCodeError(ValueError):
21
+ """Exception for an unhandled server response status code."""
osmnx/source/osmnx/_http.py ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Handle HTTP requests to web APIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging as lg
7
+ import socket
8
+ from hashlib import sha1
9
+ from pathlib import Path
10
+ from typing import Any
11
+ from urllib.parse import urlparse
12
+
13
+ import requests
14
+ from requests.exceptions import JSONDecodeError
15
+
16
+ from . import settings
17
+ from . import utils
18
+ from ._errors import InsufficientResponseError
19
+ from ._errors import ResponseStatusCodeError
20
+
21
+ # capture getaddrinfo function to use original later after mutating it
22
+ _original_getaddrinfo = socket.getaddrinfo
23
+
24
+
25
+ def _save_to_cache(
26
+ url: str,
27
+ response_json: dict[str, Any] | list[dict[str, Any]],
28
+ ok: bool, # noqa: FBT001
29
+ ) -> None:
30
+ """
31
+ Save a HTTP response JSON object to a file in the cache folder.
32
+
33
+ If request was sent to server via POST instead of GET, then `url` should
34
+ be a GET-style representation of the request. Response is only saved to a
35
+ cache file if `settings.use_cache` is True, `ok` is True, `response_json`
36
+ is not None, and `response_json` does not contain a server "remark."
37
+
38
+ Users should always pass OrderedDicts instead of dicts of parameters into
39
+ request functions, so the parameters remain in the same order each time,
40
+ producing the same URL string, and thus the same hash. Otherwise you will
41
+ get a cache miss when the URL's parameters appeared in a different order.
42
+
43
+ Parameters
44
+ ----------
45
+ url
46
+ The URL of the request.
47
+ response_json
48
+ The JSON HTTP response.
49
+ ok
50
+ A `requests.response.ok` value.
51
+ """
52
+ if settings.use_cache:
53
+ if not ok: # pragma: no cover
54
+ msg = "Did not save to cache because HTTP status code is not OK"
55
+ utils.log(msg, level=lg.WARNING)
56
+ elif isinstance(response_json, dict) and ("remark" in response_json): # pragma: no cover
57
+ msg = f"Did not save to cache because response contains remark: {response_json['remark']!r}"
58
+ utils.log(msg, lg.WARNING)
59
+ else:
60
+ # create cache folder on disk if it doesn't already exist
61
+ cache_filepath = _resolve_cache_filepath(url)
62
+ cache_filepath.parent.mkdir(parents=True, exist_ok=True)
63
+ cache_filepath.write_text(json.dumps(response_json), encoding="utf-8")
64
+ msg = f"Saved response to cache file {str(cache_filepath)!r}"
65
+ utils.log(msg, level=lg.INFO)
66
+
67
+
68
+ def _resolve_cache_filepath(key: str, extension: str = "json") -> Path:
69
+ """
70
+ Determine a cache key's corresponding cache file path.
71
+
72
+ This uses the configured `settings.cache_folder` and calculates the 160
73
+ bit SHA-1 hash digest (40 hexadecimal characters) of `key` to determine a
74
+ succinct but unique cache filename.
75
+
76
+ Parameters
77
+ ----------
78
+ key
79
+ The key for which to generate a cache file path, for example, a URL.
80
+ extension
81
+ The desired cache file's extension.
82
+
83
+ Returns
84
+ -------
85
+ cache_filepath
86
+ Cache file path corresponding to `key`.
87
+ """
88
+ digest = sha1(key.encode("utf-8")).hexdigest() # noqa: S324
89
+ return Path(settings.cache_folder) / f"{digest}.{extension}"
90
+
91
+
92
+ def _check_cache(key: str) -> Path | None:
93
+ """
94
+ Check if a key exists in the cache, and return its cache file path if so.
95
+
96
+ Parameters
97
+ ----------
98
+ key
99
+ The key to look for in the cache.
100
+
101
+ Returns
102
+ -------
103
+ cache_filepath
104
+ Filepath to cached data for `key` if it exists, otherwise None.
105
+ """
106
+ cache_filepath = _resolve_cache_filepath(key)
107
+ return cache_filepath if cache_filepath.is_file() else None
108
+
109
+
110
+ def _retrieve_from_cache(url: str) -> dict[str, Any] | list[dict[str, Any]] | None:
111
+ """
112
+ Retrieve a HTTP response JSON object from the cache if it exists.
113
+
114
+ A cache hit returns the data. A cache miss returns None.
115
+
116
+ Parameters
117
+ ----------
118
+ url
119
+ The URL of the request.
120
+
121
+ Returns
122
+ -------
123
+ response_json
124
+ The cached response for `url` if it exists, otherwise None.
125
+ """
126
+ # if the tool is configured to use the cache
127
+ if settings.use_cache:
128
+ # return cached response for this url if exists, otherwise return None
129
+ cache_filepath = _check_cache(url)
130
+ if cache_filepath is not None:
131
+ response_json: dict[str, Any] | list[dict[str, Any]]
132
+ response_json = json.loads(cache_filepath.read_text(encoding="utf-8"))
133
+ msg = f"Retrieved response from cache file {str(cache_filepath)!r}"
134
+ utils.log(msg, lg.INFO)
135
+ return response_json
136
+
137
+ return None
138
+
139
+
140
+ def _get_http_headers(
141
+ *,
142
+ user_agent: str | None = None,
143
+ referer: str | None = None,
144
+ accept_language: str | None = None,
145
+ ) -> dict[str, str]:
146
+ """
147
+ Update the default requests HTTP headers with OSMnx information.
148
+
149
+ Parameters
150
+ ----------
151
+ user_agent
152
+ The user agent. If None, use `settings.http_user_agent` value.
153
+ referer
154
+ The referer. If None, use `settings.http_referer` value.
155
+ accept_language
156
+ The accept language. If None, use `settings.http_accept_language`
157
+ value.
158
+
159
+ Returns
160
+ -------
161
+ headers
162
+ The updated HTTP headers.
163
+ """
164
+ if user_agent is None:
165
+ user_agent = settings.http_user_agent
166
+ if referer is None:
167
+ referer = settings.http_referer
168
+ if accept_language is None:
169
+ accept_language = settings.http_accept_language
170
+
171
+ info = {"User-Agent": user_agent, "referer": referer, "Accept-Language": accept_language}
172
+ headers = dict(requests.utils.default_headers())
173
+ headers.update(info)
174
+ return headers
175
+
176
+
177
+ def _resolve_host_via_doh(hostname: str) -> str:
178
+ """
179
+ Resolve hostname to IP address via Google's public DNS-over-HTTPS API.
180
+
181
+ Necessary fallback as socket.gethostbyname will not always work when using
182
+ a proxy. See https://developers.google.com/speed/public-dns/docs/doh/json
183
+ If the user has set `settings.doh_url_template=None` or if resolution
184
+ fails (e.g., due to local network blocking DNS-over-HTTPS) the hostname
185
+ itself will be returned instead. Note that this means that server slot
186
+ management may be violated: see `_config_dns` documentation for details.
187
+
188
+ Parameters
189
+ ----------
190
+ hostname
191
+ The hostname to consistently resolve the IP address of.
192
+
193
+ Returns
194
+ -------
195
+ ip_address
196
+ Resolved IP address of host, or hostname itself if resolution failed.
197
+ """
198
+ if settings.doh_url_template is None:
199
+ # if user has set the url template to None, return hostname itself
200
+ msg = "User set `doh_url_template=None`, requesting host by name"
201
+ utils.log(msg, level=lg.WARNING)
202
+ return hostname
203
+
204
+ err_msg = f"Failed to resolve {hostname!r} IP via DoH, requesting host by name"
205
+ try:
206
+ url = settings.doh_url_template.format(hostname=hostname)
207
+ response = requests.get(url, timeout=settings.requests_timeout)
208
+ data = response.json()
209
+
210
+ # if we cannot reach DoH server or resolve host, return hostname itself
211
+ except requests.exceptions.RequestException: # pragma: no cover
212
+ utils.log(err_msg, level=lg.ERROR)
213
+ return hostname
214
+
215
+ # if there were no request exceptions, return
216
+ else:
217
+ if response.ok and data["Status"] == 0:
218
+ # status 0 means NOERROR, so return the IP address
219
+ ip_address: str = data["Answer"][0]["data"]
220
+ return ip_address
221
+
222
+ # otherwise, if we cannot reach DoH server or cannot resolve host
223
+ # just return the hostname itself
224
+ utils.log(err_msg, level=lg.ERROR)
225
+ return hostname
226
+
227
+
228
+ def _config_dns(url: str) -> None:
229
+ """
230
+ Force socket.getaddrinfo to use IP address instead of hostname.
231
+
232
+ Resolves URL's hostname to an IP address so that we use the same server
233
+ for both 1) checking the necessary pause duration and 2) sending the query
234
+ itself even if there is round-robin redirecting among multiple server
235
+ machines on the server-side. Mutates the getaddrinfo function so it uses
236
+ the same IP address everytime it finds the hostname in the URL.
237
+
238
+ For example, the server overpass-api.de just redirects to one of the other
239
+ servers (currently gall.openstreetmap.de and lambert.openstreetmap.de). So
240
+ if we check the status endpoint of overpass-api.de, we may see results for
241
+ server gall, but when we submit the query itself it gets redirected to
242
+ server lambert. This could result in violating server lambert's slot
243
+ management timing.
244
+
245
+ Parameters
246
+ ----------
247
+ url
248
+ The URL to consistently resolve the IP address of.
249
+ """
250
+ hostname = _hostname_from_url(url)
251
+ try:
252
+ ip = socket.gethostbyname(hostname)
253
+ except socket.gaierror: # pragma: no cover
254
+ # may occur when using a proxy, so instead resolve IP address via DoH
255
+ msg = f"Encountered gaierror while trying to resolve {hostname!r}, trying again via DoH..."
256
+ utils.log(msg, level=lg.ERROR)
257
+ ip = _resolve_host_via_doh(hostname)
258
+
259
+ # mutate socket.getaddrinfo to map hostname -> IP address
260
+ def _getaddrinfo(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401
261
+ if hostname == next(iter(args), kwargs.get("host")):
262
+ # remove "host" from kwargs to avoid TypeError with positional argument
263
+ kwargs.pop("host", None)
264
+ msg = f"Resolved {hostname!r} to {ip!r}"
265
+ utils.log(msg, level=lg.INFO)
266
+ return _original_getaddrinfo(ip, *args[1:], **kwargs)
267
+
268
+ # otherwise
269
+ return _original_getaddrinfo(*args, **kwargs)
270
+
271
+ socket.getaddrinfo = _getaddrinfo
272
+
273
+
274
+ def _hostname_from_url(url: str) -> str:
275
+ """
276
+ Extract the hostname (domain) from a URL.
277
+
278
+ Parameters
279
+ ----------
280
+ url
281
+ The url from which to extract the hostname.
282
+
283
+ Returns
284
+ -------
285
+ hostname
286
+ The extracted hostname (domain).
287
+ """
288
+ return urlparse(url).netloc.split(":")[0]
289
+
290
+
291
+ def _parse_response(response: requests.Response) -> dict[str, Any] | list[dict[str, Any]]:
292
+ """
293
+ Parse JSON from a requests response and log the details.
294
+
295
+ Parameters
296
+ ----------
297
+ response
298
+ The response object.
299
+
300
+ Returns
301
+ -------
302
+ response_json
303
+ Value will be a dict if the response is from the Google or Overpass
304
+ APIs, and a list if the response is from the Nominatim API.
305
+ """
306
+ # log the response size and hostname
307
+ hostname = _hostname_from_url(response.url)
308
+ size_kb = len(response.content) / 1000
309
+ msg = f"Downloaded {size_kb:,.1f}kB from {hostname!r} with status {response.status_code}"
310
+ utils.log(msg, level=lg.INFO)
311
+
312
+ # parse the response to JSON and log/raise exceptions
313
+ try:
314
+ response_json: dict[str, Any] | list[dict[str, Any]] = response.json()
315
+ except JSONDecodeError as e: # pragma: no cover
316
+ msg = f"{hostname!r} responded: {response.status_code} {response.reason} {response.text}"
317
+ utils.log(msg, level=lg.ERROR)
318
+ if response.ok:
319
+ raise InsufficientResponseError(msg) from e
320
+ raise ResponseStatusCodeError(msg) from e
321
+
322
+ # log any remarks if they exist
323
+ if isinstance(response_json, dict) and "remark" in response_json: # pragma: no cover
324
+ msg = f"{hostname!r} remarked: {response_json['remark']!r}"
325
+ utils.log(msg, level=lg.WARNING)
326
+
327
+ # log if the response status_code is not OK
328
+ if not response.ok:
329
+ msg = f"{hostname!r} returned HTTP status code {response.status_code}"
330
+ utils.log(msg, level=lg.WARNING)
331
+
332
+ return response_json
osmnx/source/osmnx/_nominatim.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tools to work with the Nominatim API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging as lg
6
+ import time
7
+ from collections import OrderedDict
8
+ from typing import Any
9
+
10
+ import requests
11
+
12
+ from . import _http
13
+ from . import settings
14
+ from . import utils
15
+ from ._errors import InsufficientResponseError
16
+
17
+
18
+ def _download_nominatim_element(
19
+ query: str | dict[str, str],
20
+ *,
21
+ by_osmid: bool = False,
22
+ limit: int = 1,
23
+ polygon_geojson: bool = True,
24
+ ) -> list[dict[str, Any]]:
25
+ """
26
+ Retrieve an OSM element from the Nominatim API.
27
+
28
+ Parameters
29
+ ----------
30
+ query
31
+ Query string or structured query dict.
32
+ by_osmid
33
+ If True, treat `query` as an OSM ID lookup rather than text search.
34
+ limit
35
+ Max number of results to return.
36
+ polygon_geojson
37
+ Whether to retrieve the place's geometry from the API.
38
+
39
+ Returns
40
+ -------
41
+ response_json
42
+ The Nominatim API's response.
43
+ """
44
+ # define the parameters
45
+ params: OrderedDict[str, int | str] = OrderedDict()
46
+ params["format"] = "json"
47
+ params["polygon_geojson"] = int(polygon_geojson) # bool -> int
48
+
49
+ if by_osmid:
50
+ # if querying by OSM ID, use the lookup endpoint
51
+ if not isinstance(query, str):
52
+ msg = "`query` must be a string if `by_osmid` is True."
53
+ raise TypeError(msg)
54
+ request_type = "lookup"
55
+ params["osm_ids"] = query
56
+
57
+ else:
58
+ # if not querying by OSM ID, use the search endpoint
59
+ request_type = "search"
60
+
61
+ # prevent OSM from deduping so we get precise number of results
62
+ params["dedupe"] = 0
63
+ params["limit"] = limit
64
+
65
+ if isinstance(query, str):
66
+ params["q"] = query
67
+ elif isinstance(query, dict):
68
+ # add query keys in alphabetical order so URL is the same string
69
+ # each time, for caching purposes
70
+ for key in sorted(query):
71
+ params[key] = query[key]
72
+ else: # pragma: no cover
73
+ msg = "Each query must be a dict or a string." # type: ignore[unreachable]
74
+ raise TypeError(msg)
75
+
76
+ # request the URL, return the JSON
77
+ return _nominatim_request(params=params, request_type=request_type)
78
+
79
+
80
+ def _nominatim_request(
81
+ params: OrderedDict[str, int | str],
82
+ *,
83
+ request_type: str = "search",
84
+ ) -> list[dict[str, Any]]:
85
+ """
86
+ Send a HTTP GET request to the Nominatim API and return response.
87
+
88
+ Parameters
89
+ ----------
90
+ params
91
+ Key-value pairs of parameters.
92
+ request_type
93
+ Which Nominatim API endpoint to query, one of {"search", "reverse",
94
+ "lookup"}.
95
+
96
+ Returns
97
+ -------
98
+ response_json
99
+ The Nominatim API's response.
100
+ """
101
+ if request_type not in {"search", "reverse", "lookup"}: # pragma: no cover
102
+ msg = "Nominatim `request_type` must be 'search', 'reverse', or 'lookup'."
103
+ raise ValueError(msg)
104
+
105
+ # add nominatim API key to params if one has been provided in settings
106
+ if settings.nominatim_key is not None:
107
+ params["key"] = settings.nominatim_key
108
+
109
+ # prepare Nominatim API URL and see if request already exists in cache
110
+ url = settings.nominatim_url.rstrip("/") + "/" + request_type
111
+ prepared_url = str(requests.Request("GET", url, params=params).prepare().url)
112
+ cached_response_json = _http._retrieve_from_cache(prepared_url)
113
+ if isinstance(cached_response_json, list):
114
+ return cached_response_json
115
+
116
+ # how long to pause before request, in seconds. Per the Nominatim usage
117
+ # policy: "an absolute maximum of 1 request per second" is allowed.
118
+ pause = 1
119
+ hostname = _http._hostname_from_url(url)
120
+ msg = f"Pausing {pause} second(s) before making HTTP GET request to {hostname!r}"
121
+ utils.log(msg, level=lg.INFO)
122
+ time.sleep(pause)
123
+
124
+ # transmit the HTTP GET request
125
+ msg = f"Get {prepared_url} with timeout={settings.requests_timeout}"
126
+ utils.log(msg, level=lg.INFO)
127
+ response = requests.get(
128
+ url,
129
+ params=params,
130
+ timeout=settings.requests_timeout,
131
+ headers=_http._get_http_headers(),
132
+ **settings.requests_kwargs,
133
+ )
134
+
135
+ # handle 429 and 504 errors by pausing then recursively re-trying request
136
+ if response.status_code in {429, 504}: # pragma: no cover
137
+ error_pause = 55
138
+ msg = (
139
+ f"{hostname!r} responded {response.status_code} {response.reason}: "
140
+ f"we'll retry in {error_pause} secs"
141
+ )
142
+ utils.log(msg, level=lg.WARNING)
143
+ time.sleep(error_pause)
144
+ return _nominatim_request(params, request_type=request_type)
145
+
146
+ response_json = _http._parse_response(response)
147
+ if not isinstance(response_json, list):
148
+ msg = "Nominatim API did not return a list of results."
149
+ raise InsufficientResponseError(msg)
150
+ _http._save_to_cache(prepared_url, response_json, response.ok)
151
+ return response_json
osmnx/source/osmnx/_osm_xml.py ADDED
@@ -0,0 +1,439 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Read/write OSM XML files.
3
+
4
+ For file format information see https://wiki.openstreetmap.org/wiki/OSM_XML
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import bz2
10
+ import gzip
11
+ import logging as lg
12
+ from contextlib import contextmanager
13
+ from importlib.metadata import version as metadata_version
14
+ from pathlib import Path
15
+ from typing import TYPE_CHECKING
16
+ from typing import Any
17
+ from typing import TextIO
18
+ from warnings import warn
19
+ from xml.etree.ElementTree import Element
20
+ from xml.etree.ElementTree import ElementTree
21
+ from xml.etree.ElementTree import SubElement
22
+ from xml.etree.ElementTree import parse as etree_parse
23
+ from xml.sax import parse as sax_parse
24
+ from xml.sax.handler import ContentHandler
25
+
26
+ import networkx as nx
27
+ import pandas as pd
28
+
29
+ from . import convert
30
+ from . import projection
31
+ from . import settings
32
+ from . import truncate
33
+ from . import utils
34
+ from ._errors import GraphSimplificationError
35
+
36
+ if TYPE_CHECKING:
37
+ from collections.abc import Iterator
38
+ from xml.sax.xmlreader import AttributesImpl
39
+
40
+ import geopandas as gpd
41
+
42
+
43
+ # default values for standard "node" and "way" XML subelement attributes
44
+ # see: https://wiki.openstreetmap.org/wiki/Elements#Common_attributes
45
+ ATTR_DEFAULTS = {
46
+ "changeset": "1",
47
+ "timestamp": utils.ts(style="iso8601"),
48
+ "uid": "1",
49
+ "user": "OSMnx",
50
+ "version": "1",
51
+ "visible": "true",
52
+ }
53
+
54
+ # default values for standard "osm" root XML element attributes
55
+ # current OSM editing API version: https://wiki.openstreetmap.org/wiki/API
56
+ ROOT_ATTR_DEFAULTS = {
57
+ "attribution": "https://www.openstreetmap.org/copyright",
58
+ "copyright": "OpenStreetMap and contributors",
59
+ "generator": f"OSMnx {metadata_version('osmnx')}",
60
+ "license": "https://opendatacommons.org/licenses/odbl/1-0/",
61
+ "version": "0.6",
62
+ }
63
+
64
+
65
+ class _OSMContentHandler(ContentHandler):
66
+ """
67
+ SAX content handler for OSM XML.
68
+
69
+ Builds an Overpass-like response JSON object in self.object. For format
70
+ notes, see https://wiki.openstreetmap.org/wiki/OSM_XML and
71
+ https://overpass-api.de
72
+ """
73
+
74
+ def __init__(self) -> None:
75
+ self._element: dict[str, Any] | None = None
76
+ self.object: dict[str, Any] = {"elements": []}
77
+
78
+ def startElement(self, name: str, attrs: AttributesImpl) -> None: # noqa: N802
79
+ # identify node/way/relation attrs to convert from string to numeric
80
+ float_attrs = {"lat", "lon"}
81
+ int_attrs = {"changeset", "id", "uid", "version"}
82
+
83
+ if name == "osm":
84
+ self.object.update({k: v for k, v in attrs.items() if k in ROOT_ATTR_DEFAULTS})
85
+
86
+ elif name in {"node", "way"}:
87
+ self._element = dict(type=name, tags={}, **attrs)
88
+ if name == "way":
89
+ self._element["nodes"] = []
90
+ self._element.update({k: float(v) for k, v in attrs.items() if k in float_attrs})
91
+ self._element.update({k: int(v) for k, v in attrs.items() if k in int_attrs})
92
+
93
+ elif name == "relation":
94
+ self._element = dict(type=name, tags={}, members=[], **attrs)
95
+ self._element.update({k: int(v) for k, v in attrs.items() if k in int_attrs})
96
+
97
+ elif name == "tag":
98
+ self._element["tags"].update({attrs["k"]: attrs["v"]}) # type: ignore[index]
99
+
100
+ elif name == "nd":
101
+ self._element["nodes"].append(int(attrs["ref"])) # type: ignore[index]
102
+
103
+ elif name == "member":
104
+ self._element["members"].append( # type: ignore[index]
105
+ {k: (int(v) if k == "ref" else v) for k, v in attrs.items()},
106
+ )
107
+
108
+ def endElement(self, name: str) -> None: # noqa: N802
109
+ if name in {"node", "way", "relation"}:
110
+ self.object["elements"].append(self._element)
111
+
112
+
113
+ @contextmanager
114
+ def _open_file(filepath: Path, encoding: str) -> Iterator[TextIO]:
115
+ """
116
+ Open a file and return a file object, optionally handling bz2 or gz files.
117
+
118
+ Uses a wrapper context manager to yield the file object to ensure the file
119
+ will always get closed when the caller is finished with it.
120
+
121
+ Parameters
122
+ ----------
123
+ filepath
124
+ Path to file.
125
+ encoding
126
+ The file's character encoding.
127
+
128
+ Returns
129
+ -------
130
+ file
131
+ The file handle.
132
+ """
133
+ if filepath.suffix == ".bz2":
134
+ with bz2.open(filepath, mode="rt", encoding=encoding) as file:
135
+ yield file
136
+ elif filepath.suffix == ".gz":
137
+ with gzip.open(filepath, mode="rt", encoding=encoding) as file:
138
+ yield file
139
+ else:
140
+ with filepath.open(mode="rt", encoding=encoding) as file:
141
+ yield file
142
+
143
+
144
+ def _overpass_json_from_xml(filepath: Path, encoding: str) -> dict[str, Any]:
145
+ """
146
+ Read OSM XML data from file and return Overpass-like JSON.
147
+
148
+ Parameters
149
+ ----------
150
+ filepath
151
+ Path to file containing OSM XML data.
152
+ encoding
153
+ The XML file's character encoding.
154
+
155
+ Returns
156
+ -------
157
+ response_json
158
+ A parsed JSON response from the Overpass API.
159
+ """
160
+ with _open_file(filepath, encoding) as file:
161
+ # warn if this XML file was generated by OSMnx itself
162
+ root_attrs = etree_parse(file).getroot().attrib # noqa: S314
163
+ if "generator" in root_attrs and "OSMnx" in root_attrs["generator"]:
164
+ msg = (
165
+ "The XML file you are loading appears to have been generated "
166
+ "by OSMnx: this use case is not supported and may not behave "
167
+ "as expected. To save/load graphs to/from disk for later use "
168
+ "in OSMnx, use the `io.save_graphml` and `io.load_graphml` "
169
+ "functions instead. Refer to the documentation for details."
170
+ )
171
+ warn(msg, category=UserWarning, stacklevel=2)
172
+
173
+ # move back to beginning of file, then parse XML to Overpass-like JSON
174
+ file.seek(0)
175
+ handler = _OSMContentHandler()
176
+ sax_parse(file, handler) # noqa: S317
177
+
178
+ return handler.object
179
+
180
+
181
+ def _save_graph_xml(
182
+ G: nx.MultiDiGraph,
183
+ filepath: str | Path | None,
184
+ way_tag_aggs: dict[str, Any] | None,
185
+ encoding: str = "utf-8",
186
+ ) -> None:
187
+ """
188
+ Save graph to disk as an OSM XML file.
189
+
190
+ Parameters
191
+ ----------
192
+ G
193
+ Unsimplified, unprojected graph to save as an OSM XML file.
194
+ filepath
195
+ Path to the saved file including extension. If None, use default
196
+ `settings.data_folder/graph.osm`.
197
+ way_tag_aggs
198
+ Keys are OSM way tag keys and values are aggregation functions
199
+ (anything accepted as an argument by `pandas.agg`). Allows user to
200
+ aggregate graph edge attribute values into single OSM way values. If
201
+ None, or if some tag's key does not exist in the dict, the way
202
+ attribute will be assigned the value of the first edge of the way.
203
+ encoding
204
+ The character encoding of the saved OSM XML file.
205
+ """
206
+ # default "oneway" value used to fill this tag where missing
207
+ ONEWAY = False
208
+
209
+ # round lat/lon coordinates to 7 decimals (approx 5 to 10 mm resolution)
210
+ PRECISION = 7
211
+
212
+ # warn user if ox.settings.all_oneway is not currently True (but maybe it
213
+ # was when they created the graph)
214
+ if not settings.all_oneway:
215
+ msg = "Make sure graph was created with `ox.settings.all_oneway=True` to save as OSM XML."
216
+ warn(msg, category=UserWarning, stacklevel=2)
217
+
218
+ # warn user if graph is projected
219
+ if projection.is_projected(G.graph["crs"]):
220
+ msg = (
221
+ "Graph should be unprojected to save as OSM XML: the existing "
222
+ "projected x-y coordinates will be saved as lat-lon node attributes. "
223
+ "Project your graph back to lat-lon to avoid this."
224
+ )
225
+ warn(msg, category=UserWarning, stacklevel=2)
226
+
227
+ # raise error if graph has been simplified
228
+ if G.graph.get("simplified", False):
229
+ msg = "Graph must be unsimplified to save as OSM XML."
230
+ raise GraphSimplificationError(msg)
231
+
232
+ # set default filepath if None was provided
233
+ filepath = Path(settings.data_folder) / "graph.osm" if filepath is None else Path(filepath)
234
+ filepath.parent.mkdir(parents=True, exist_ok=True)
235
+
236
+ # convert graph to node/edge gdfs and create dict of spatial bounds
237
+ gdf_nodes, gdf_edges = convert.graph_to_gdfs(G, fill_edge_geometry=False)
238
+ coords = [str(round(c, PRECISION)) for c in gdf_nodes.union_all().bounds]
239
+ bounds = dict(zip(["minlon", "minlat", "maxlon", "maxlat"], coords, strict=True))
240
+
241
+ # add default values (if missing) for standard attrs
242
+ for gdf in (gdf_nodes, gdf_edges):
243
+ for col, value in ATTR_DEFAULTS.items():
244
+ if col not in gdf.columns:
245
+ gdf[col] = value
246
+ else:
247
+ gdf[col] = gdf[col].fillna(value)
248
+
249
+ # transform nodes gdf to meet OSM XML spec
250
+ # 1) reset index (osmid) then rename osmid, x, and y columns
251
+ # 2) round lat/lon coordinates
252
+ # 3) drop unnecessary geometry column
253
+ gdf_nodes = gdf_nodes.reset_index().rename(columns={"osmid": "id", "x": "lon", "y": "lat"})
254
+ gdf_nodes[["lon", "lat"]] = gdf_nodes[["lon", "lat"]].round(PRECISION)
255
+ gdf_nodes = gdf_nodes.drop(columns=["geometry"])
256
+
257
+ # transform edges gdf to meet OSM XML spec
258
+ # 1) fill and convert oneway bools to strings
259
+ # 2) rename osmid column (but keep (u, v, k) index for processing)
260
+ # 3) drop unnecessary geometry column
261
+ if "oneway" in gdf_edges.columns:
262
+ gdf_edges["oneway"] = gdf_edges["oneway"].fillna(ONEWAY).replace({True: "yes", False: "no"})
263
+ gdf_edges = gdf_edges.rename(columns={"osmid": "id"}).drop(columns=["geometry"])
264
+
265
+ # create parent XML element then add bounds, nodes, ways as subelements
266
+ element = Element("osm", attrib=ROOT_ATTR_DEFAULTS)
267
+ _ = SubElement(element, "bounds", attrib=bounds)
268
+ _add_nodes_xml(element, gdf_nodes)
269
+ _add_ways_xml(element, gdf_edges, way_tag_aggs)
270
+
271
+ # write to disk
272
+ ElementTree(element).write(filepath, encoding=encoding, xml_declaration=True)
273
+ msg = f"Saved graph as OSM XML file at {str(filepath)!r}"
274
+ utils.log(msg, level=lg.INFO)
275
+
276
+
277
+ def _add_nodes_xml(
278
+ parent: Element,
279
+ gdf_nodes: gpd.GeoDataFrame,
280
+ ) -> None:
281
+ """
282
+ Add graph nodes as subelements of an XML parent element.
283
+
284
+ Parameters
285
+ ----------
286
+ parent
287
+ The XML parent element.
288
+ gdf_nodes
289
+ A GeoDataFrame of graph nodes.
290
+ """
291
+ node_tags = set(settings.useful_tags_node)
292
+ node_attrs = {"id", "lat", "lon"}.union(ATTR_DEFAULTS)
293
+
294
+ # add each node attrs dict as a SubElement of parent
295
+ for node in gdf_nodes.to_dict(orient="records"):
296
+ attrs = {k: str(node[k]) for k in node_attrs if pd.notna(node[k])}
297
+ node_element = SubElement(parent, "node", attrib=attrs)
298
+
299
+ # add each node tag dict as its own SubElement of the node SubElement
300
+ # for vals that are non-null (or list if node consolidation was done)
301
+ tags = (
302
+ {"k": k, "v": str(node[k])}
303
+ for k in node_tags & node.keys()
304
+ if isinstance(node[k], list) or pd.notna(node[k])
305
+ )
306
+ for tag in tags:
307
+ _ = SubElement(node_element, "tag", attrib=tag)
308
+
309
+
310
+ def _add_ways_xml(
311
+ parent: Element,
312
+ gdf_edges: gpd.GeoDataFrame,
313
+ way_tag_aggs: dict[str, Any] | None,
314
+ ) -> None:
315
+ """
316
+ Add graph edges (grouped as ways) as subelements of an XML parent element.
317
+
318
+ Parameters
319
+ ----------
320
+ parent
321
+ The XML parent element.
322
+ gdf_edges
323
+ A GeoDataFrame of graph edges with OSM way "id" column for grouping
324
+ edges into ways.
325
+ way_tag_aggs
326
+ Keys are OSM way tag keys and values are aggregation functions
327
+ (anything accepted as an argument by `pandas.agg`). Allows user to
328
+ aggregate graph edge attribute values into single OSM way values. If
329
+ None, or if some tag's key does not exist in the dict, the way
330
+ attribute will be assigned the value of the first edge of the way.
331
+ """
332
+ way_tags = set(settings.useful_tags_way)
333
+ way_attrs = list({"id"}.union(ATTR_DEFAULTS))
334
+
335
+ for osmid, way in gdf_edges.groupby("id"):
336
+ # STEP 1: add the way and its attrs as a "way" subelement of the
337
+ # parent element
338
+ attrs = way[way_attrs].iloc[0].astype(str).to_dict()
339
+ way_element = SubElement(parent, "way", attrib=attrs)
340
+
341
+ # STEP 2: add the way's edges' node IDs as "nd" subelements of the
342
+ # "way" subelement. if way contains more than 1 edge, sort the nodes
343
+ # topologically, otherwise just add node "u" then "v" from index.
344
+ if len(way) == 1:
345
+ nodes = way.index[0][:2]
346
+ else:
347
+ nodes = _sort_nodes(nx.MultiDiGraph(way.index.to_list()), osmid)
348
+ for node in nodes:
349
+ _ = SubElement(way_element, "nd", attrib={"ref": str(node)})
350
+
351
+ # STEP 3: add way's edges' tags as "tag" subelements of the "way"
352
+ # subelement. if an agg function was provided for a tag, apply it to
353
+ # the values of the edges in the way. if no agg function was provided
354
+ # for a tag, just use the value from first edge in way.
355
+ for tag in way_tags.intersection(way.columns):
356
+ if way_tag_aggs is not None and tag in way_tag_aggs:
357
+ value = way[tag].agg(way_tag_aggs[tag])
358
+ else:
359
+ value = way[tag].iloc[0]
360
+ if pd.notna(value):
361
+ _ = SubElement(way_element, "tag", attrib={"k": tag, "v": str(value)})
362
+
363
+
364
+ def _sort_nodes(G: nx.MultiDiGraph, osmid: int) -> list[int]:
365
+ """
366
+ Topologically sort the nodes of an OSM way.
367
+
368
+ Parameters
369
+ ----------
370
+ G
371
+ The graph representing the OSM way.
372
+ osmid
373
+ The OSM way ID.
374
+
375
+ Returns
376
+ -------
377
+ ordered_nodes
378
+ The way's node IDs in topologically sorted order.
379
+ """
380
+ try:
381
+ ordered_nodes = list(nx.topological_sort(G))
382
+
383
+ except nx.NetworkXUnfeasible:
384
+ # if it couldn't topologically sort the nodes, the way probably
385
+ # contains a cycle. try removing an edge to break the cycle. first,
386
+ # look for multiple edges emanating from the same source node
387
+ insert_before = True
388
+ edges = [
389
+ edge
390
+ for source in [node for node, degree in G.out_degree() if degree > 1]
391
+ for edge in G.out_edges(source, keys=True)
392
+ ]
393
+
394
+ # if none found, then look for multiple edges pointing at the same
395
+ # target node instead
396
+ if len(edges) == 0:
397
+ insert_before = False
398
+ edges = [
399
+ edge
400
+ for target in [node for node, degree in G.in_degree() if degree > 1]
401
+ for edge in G.in_edges(target, keys=True)
402
+ ]
403
+
404
+ # if still none, then take the first edge of the way: the entire
405
+ # way could just be a cycle in which each node appears once
406
+ if len(edges) == 0:
407
+ edges = [next(iter(G.edges))]
408
+
409
+ # remove one edge at a time and, if the graph remains connected, exit
410
+ # the loop and check if we are able to topologically sort the nodes
411
+ for edge in edges:
412
+ G_ = G.copy()
413
+ G_.remove_edge(*edge)
414
+ if nx.is_weakly_connected(G_):
415
+ break
416
+
417
+ try:
418
+ ordered_nodes = list(nx.topological_sort(G_))
419
+
420
+ # re-insert (before or after its neighbor as needed) the duplicate
421
+ # source or target node from the edge we removed
422
+ dupe_node = edge[0] if insert_before else edge[1]
423
+ neighbor = edge[1] if insert_before else edge[0]
424
+ position = ordered_nodes.index(neighbor)
425
+ position = position if insert_before else position + 1
426
+ ordered_nodes.insert(position, dupe_node)
427
+
428
+ except nx.NetworkXUnfeasible:
429
+ # if it failed again, this way probably contains multiple cycles,
430
+ # so remove a cycle then try to sort the nodes again, recursively.
431
+ # note this is destructive and will be missing in the saved data.
432
+ G_ = G.copy()
433
+ G_.remove_edges_from(nx.find_cycle(G_))
434
+ G_ = truncate.largest_component(G_)
435
+ ordered_nodes = _sort_nodes(G_, osmid)
436
+ msg = f"Had to remove a cycle from way {osmid!r} for topological sort"
437
+ utils.log(msg, level=lg.WARNING)
438
+
439
+ return ordered_nodes
osmnx/source/osmnx/_overpass.py ADDED
@@ -0,0 +1,493 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tools to work with the Overpass API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import datetime as dt
6
+ import logging as lg
7
+ import time
8
+ from collections import OrderedDict
9
+ from typing import TYPE_CHECKING
10
+ from typing import Any
11
+
12
+ import numpy as np
13
+ import requests
14
+ from requests.exceptions import ConnectionError as RequestsConnectionError
15
+
16
+ from . import _http
17
+ from . import projection
18
+ from . import settings
19
+ from . import utils
20
+ from . import utils_geo
21
+ from ._errors import InsufficientResponseError
22
+
23
+ if TYPE_CHECKING:
24
+ from collections.abc import Iterator
25
+
26
+ from shapely import MultiPolygon
27
+ from shapely import Polygon
28
+
29
+
30
+ def _get_network_filter(network_type: str) -> str:
31
+ """
32
+ Create a filter to query Overpass for the specified network type.
33
+
34
+ The filter queries Overpass for every OSM way with a "highway" tag but
35
+ excludes ways that are incompatible with the requested network type. You
36
+ can choose from the following types:
37
+
38
+ "all" retrieves all public and private-access ways currently in use,
39
+ excluding those that represent areas either explicitly (area=yes) or by
40
+ convention (rest_area, services).
41
+
42
+ "all_public" retrieves all public ways currently in use.
43
+
44
+ "bike" retrieves public bikeable ways and excludes foot ways, motor ways,
45
+ and anything tagged biking=no.
46
+
47
+ "drive" retrieves public drivable streets and excludes service roads,
48
+ anything tagged motor=no, and certain non-service roads tagged as
49
+ providing certain services (such as alleys or driveways).
50
+
51
+ "drive_service" retrieves public drivable streets including service roads
52
+ but excludes certain services (such as parking or emergency access).
53
+
54
+ "walk" retrieves public walkable ways and excludes cycle ways, motor ways,
55
+ and anything tagged foot=no. It includes service roads like parking lot
56
+ aisles and alleys that you can walk on even if they are unpleasant walks.
57
+
58
+ Parameters
59
+ ----------
60
+ network_type
61
+ {"all", "all_public", "bike", "drive", "drive_service", "walk"}
62
+ What type of street network to retrieve.
63
+
64
+ Returns
65
+ -------
66
+ way_filter
67
+ The Overpass query filter.
68
+ """
69
+ # define built-in queries to send to the API. specifying way["highway"]
70
+ # means that all ways returned must have a highway tag. the filters then
71
+ # remove ways by tag/value.
72
+ filters = {}
73
+
74
+ # driving: filter out un-drivable roads, service roads, private ways, and
75
+ # anything tagged motor=no. also filter out any non-service roads that are
76
+ # tagged as providing certain services
77
+ filters["drive"] = (
78
+ f'["highway"]["area"!~"yes"]{settings.default_access}'
79
+ f'["highway"!~"abandoned|bridleway|bus_guideway|construction|corridor|'
80
+ f"cycleway|elevator|escalator|footway|no|path|pedestrian|planned|platform|"
81
+ f'proposed|raceway|razed|rest_area|service|services|steps|track"]'
82
+ f'["motor_vehicle"!~"no"]["motorcar"!~"no"]'
83
+ f'["service"!~"alley|driveway|emergency_access|parking|parking_aisle|private"]'
84
+ )
85
+
86
+ # drive+service: allow ways tagged 'service' but filter out certain types
87
+ filters["drive_service"] = (
88
+ f'["highway"]["area"!~"yes"]{settings.default_access}'
89
+ f'["highway"!~"abandoned|bridleway|bus_guideway|construction|corridor|'
90
+ f"cycleway|elevator|escalator|footway|no|path|pedestrian|planned|platform|"
91
+ f'proposed|raceway|razed|rest_area|services|steps|track"]'
92
+ f'["motor_vehicle"!~"no"]["motorcar"!~"no"]'
93
+ f'["service"!~"emergency_access|parking|parking_aisle|private"]'
94
+ )
95
+
96
+ # walking: filter out cycle ways, motor ways, private ways, and anything
97
+ # tagged foot=no. allow service roads, permitting things like parking lot
98
+ # aisles, alleys, etc that you *can* walk on even if they're not exactly
99
+ # pleasant walks. some cycleways may allow pedestrians, but this filter
100
+ # ignores such cycleways.
101
+ filters["walk"] = (
102
+ f'["highway"]["area"!~"yes"]{settings.default_access}'
103
+ f'["highway"!~"abandoned|bus_guideway|construction|cycleway|motor|no|planned|'
104
+ f'platform|proposed|raceway|razed|rest_area|services"]'
105
+ f'["foot"!~"no"]["service"!~"private"]'
106
+ f'["sidewalk"!~"separate"]["sidewalk:both"!~"separate"]'
107
+ f'["sidewalk:left"!~"separate"]["sidewalk:right"!~"separate"]'
108
+ )
109
+
110
+ # biking: filter out foot ways, motor ways, private ways, and anything
111
+ # tagged biking=no
112
+ filters["bike"] = (
113
+ f'["highway"]["area"!~"yes"]{settings.default_access}'
114
+ f'["highway"!~"abandoned|bus_guideway|construction|corridor|elevator|'
115
+ f"escalator|footway|motor|no|planned|platform|proposed|raceway|razed|"
116
+ f'rest_area|services|steps"]'
117
+ f'["bicycle"!~"no"]["service"!~"private"]'
118
+ )
119
+
120
+ # to download all public ways, just filter out everything not currently in
121
+ # use or that is private-access only
122
+ filters["all_public"] = (
123
+ f'["highway"]["area"!~"yes"]{settings.default_access}'
124
+ f'["highway"!~"abandoned|construction|no|planned|platform|proposed|raceway|'
125
+ f'razed|rest_area|services"]'
126
+ f'["service"!~"private"]'
127
+ )
128
+
129
+ # to download all ways, including private-access ones, just filter out
130
+ # everything not currently in use
131
+ filters["all"] = (
132
+ '["highway"]["area"!~"yes"]["highway"!~"abandoned|construction|no|planned|'
133
+ 'platform|proposed|raceway|razed|rest_area|services"]'
134
+ )
135
+
136
+ if network_type in filters:
137
+ way_filter = filters[network_type]
138
+ else: # pragma: no cover
139
+ msg = f"Unrecognized network_type {network_type!r}."
140
+ raise ValueError(msg)
141
+
142
+ return way_filter
143
+
144
+
145
+ def _get_overpass_pause(
146
+ base_endpoint: str,
147
+ *,
148
+ recursion_pause: float = 5,
149
+ default_pause: float = 60,
150
+ ) -> float:
151
+ """
152
+ Retrieve a pause duration from the Overpass API status endpoint.
153
+
154
+ Check the Overpass API status endpoint to determine how long to wait until
155
+ the next slot is available. You can disable this via the `settings`
156
+ module's `overpass_rate_limit` setting.
157
+
158
+ Parameters
159
+ ----------
160
+ base_endpoint
161
+ Base Overpass API URL (without "/status" at the end).
162
+ recursion_pause
163
+ How long to wait between recursive calls if the server is currently
164
+ running a query.
165
+ default_pause
166
+ If a fatal error occurs, fall back on this liberal pause duration.
167
+
168
+ Returns
169
+ -------
170
+ pause
171
+ The current pause duration specified by the Overpass status endpoint.
172
+ """
173
+ # if overpass rate limiting is False, then there is zero pause
174
+ if not settings.overpass_rate_limit:
175
+ return 0
176
+
177
+ url = base_endpoint.rstrip("/") + "/status"
178
+
179
+ # try to retrieve the URL
180
+ try:
181
+ response = requests.get(
182
+ url,
183
+ headers=_http._get_http_headers(),
184
+ timeout=settings.requests_timeout,
185
+ **settings.requests_kwargs,
186
+ )
187
+ response_text = response.text
188
+ except RequestsConnectionError as e: # pragma: no cover
189
+ # cannot reach status endpoint: log error and return default duration
190
+ msg = f"Unable to reach {url}, {e}"
191
+ utils.log(msg, level=lg.ERROR)
192
+ return default_pause
193
+
194
+ # try to parse the output
195
+ try:
196
+ status = response_text.split("\n")[4]
197
+ status_first_part = status.split(" ")[0]
198
+ except (AttributeError, IndexError, ValueError): # pragma: no cover
199
+ # cannot parse output: log error and return default duration
200
+ msg = f"Unable to parse {url} response: {response_text}"
201
+ utils.log(msg, level=lg.ERROR)
202
+ return default_pause
203
+
204
+ # determine the current status of the server
205
+ try:
206
+ # if first token is numeric, it's how many slots you have available,
207
+ # no wait required
208
+ _ = int(status_first_part) # number of available slots
209
+ pause: float = 0
210
+
211
+ except ValueError: # pragma: no cover
212
+ # if first token is 'Slot', it tells you when your slot will be free
213
+ if status_first_part == "Slot":
214
+ utc_time_str = status.split(" ")[3]
215
+ pattern = "%Y-%m-%dT%H:%M:%SZ,"
216
+ utc_time = dt.datetime.strptime(utc_time_str, pattern).replace(tzinfo=dt.UTC)
217
+ utc_now = dt.datetime.now(tz=dt.UTC)
218
+ seconds = int(np.ceil((utc_time - utc_now).total_seconds()))
219
+ pause = max(seconds, 1)
220
+
221
+ # if first token is 'Currently', it is currently running a query so
222
+ # check back in recursion_pause seconds
223
+ elif status_first_part == "Currently":
224
+ time.sleep(recursion_pause)
225
+ pause = _get_overpass_pause(base_endpoint)
226
+
227
+ # any other status is unrecognized: log error, return default duration
228
+ else:
229
+ msg = f"Unrecognized server status: {status!r}"
230
+ utils.log(msg, level=lg.ERROR)
231
+ return default_pause
232
+
233
+ return pause
234
+
235
+
236
+ def _make_overpass_settings() -> str:
237
+ """
238
+ Make settings string to send in Overpass query.
239
+
240
+ Returns
241
+ -------
242
+ overpass_settings
243
+ The `settings.overpass_settings` string formatted with "timeout" and
244
+ "maxsize" values.
245
+ """
246
+ maxsize = "" if settings.overpass_memory is None else f"[maxsize:{settings.overpass_memory}]"
247
+ return settings.overpass_settings.format(timeout=settings.requests_timeout, maxsize=maxsize)
248
+
249
+
250
+ def _make_overpass_polygon_coord_strs(polygon: Polygon | MultiPolygon) -> list[str]:
251
+ """
252
+ Subdivide query polygon and return list of coordinate strings.
253
+
254
+ Project to UTM, divide `polygon` up into sub-polygons if area exceeds a
255
+ max size (in meters), project back to lat-lon, then get a list of
256
+ polygon(s) exterior coordinates. Ignore interior ("holes") coordinates.
257
+
258
+ Parameters
259
+ ----------
260
+ polygon
261
+ The (Multi)Polygon to convert to exterior coordinate strings.
262
+
263
+ Returns
264
+ -------
265
+ coord_strs
266
+ Exterior coordinates of polygon(s).
267
+ """
268
+ # subdivide the polygon if its area exceeds max size
269
+ # this results in a multipolygon of 1+ constituent polygons
270
+ poly_proj, crs_proj = projection.project_geometry(polygon)
271
+ multi_poly_proj = utils_geo._consolidate_subdivide_geometry(poly_proj)
272
+ multi_poly, _ = projection.project_geometry(multi_poly_proj, crs=crs_proj, to_latlong=True)
273
+
274
+ # then extract each's exterior coords to the string format Overpass
275
+ # expects, rounding lats and lons to 6 decimals (approx 5 to 10 cm
276
+ # resolution) so we can hash and cache URL strings consistently
277
+ coord_strs = []
278
+ for geom in multi_poly.geoms:
279
+ x, y = geom.exterior.xy
280
+ coord_list = [f"{xy[1]:.6f}{' '}{xy[0]:.6f}" for xy in zip(x, y, strict=True)]
281
+ coord_strs.append(" ".join(coord_list))
282
+
283
+ return coord_strs
284
+
285
+
286
+ def _create_overpass_features_query( # noqa: PLR0912
287
+ polygon_coord_str: str,
288
+ tags: dict[str, bool | str | list[str]],
289
+ ) -> str:
290
+ """
291
+ Create an Overpass features query string based on tags.
292
+
293
+ Parameters
294
+ ----------
295
+ polygon_coord_str
296
+ The lat lon coordinates.
297
+ tags
298
+ Tags used for finding elements in the search area.
299
+
300
+ Returns
301
+ -------
302
+ query
303
+ The Overpass features query.
304
+ """
305
+ # create overpass settings string
306
+ overpass_settings = _make_overpass_settings()
307
+
308
+ # make sure every value in dict is bool, str, or list of str
309
+ err_msg = "`tags` must be a dict with values of bool, str, or list of str."
310
+ if not isinstance(tags, dict): # pragma: no cover
311
+ raise TypeError(err_msg)
312
+
313
+ tags_dict: dict[str, bool | str | list[str]] = {}
314
+ for key, value in tags.items():
315
+ if isinstance(value, bool):
316
+ tags_dict[key] = value
317
+
318
+ elif isinstance(value, str):
319
+ tags_dict[key] = [value]
320
+
321
+ elif isinstance(value, list):
322
+ if not all(isinstance(s, str) for s in value): # pragma: no cover
323
+ raise TypeError(err_msg)
324
+ tags_dict[key] = value
325
+
326
+ else: # pragma: no cover
327
+ raise TypeError(err_msg)
328
+
329
+ # convert the tags dict into a list of {tag:value} dicts
330
+ tags_list: list[dict[str, bool | str | list[str]]] = []
331
+ for key, value in tags_dict.items():
332
+ if isinstance(value, bool):
333
+ tags_list.append({key: value})
334
+ else:
335
+ for value_item in value:
336
+ tags_list.append({key: value_item}) # noqa: PERF401
337
+
338
+ # add node/way/relation query components one at a time
339
+ components = []
340
+ for d in tags_list:
341
+ for key, value in d.items():
342
+ if isinstance(value, bool):
343
+ # if bool (ie, True) just pass the key, no value
344
+ tag_str = f"[{key!r}](poly:{polygon_coord_str!r});(._;>;);"
345
+ else:
346
+ # otherwise, pass "key"="value"
347
+ tag_str = f"[{key!r}={value!r}](poly:{polygon_coord_str!r});(._;>;);"
348
+
349
+ for kind in ("node", "way", "relation"):
350
+ components.append(f"({kind}{tag_str});") # noqa: PERF401
351
+
352
+ # finalize query and return
353
+ components_str = "".join(components)
354
+ return f"{overpass_settings};({components_str});out;"
355
+
356
+
357
+ def _download_overpass_network(
358
+ polygon: Polygon | MultiPolygon,
359
+ network_type: str,
360
+ custom_filter: str | list[str] | None,
361
+ ) -> Iterator[dict[str, Any]]:
362
+ """
363
+ Retrieve networked ways and nodes within boundary from the Overpass API.
364
+
365
+ Parameters
366
+ ----------
367
+ polygon
368
+ The boundary to fetch the network ways/nodes within.
369
+ network_type
370
+ What type of street network to get if `custom_filter` is None.
371
+ custom_filter
372
+ A custom "ways" filter to be used instead of `network_type` presets.
373
+
374
+ Yields
375
+ ------
376
+ response_json
377
+ JSON response from the Overpass server.
378
+ """
379
+ # create filter(s) to exclude certain kinds of ways based on the requested
380
+ # network_type, if provided, otherwise use custom_filter
381
+ way_filters = []
382
+ if isinstance(custom_filter, list):
383
+ way_filters = custom_filter
384
+ elif isinstance(custom_filter, str):
385
+ way_filters = [custom_filter]
386
+ else:
387
+ way_filters = [_get_network_filter(network_type)]
388
+
389
+ # create overpass settings string
390
+ overpass_settings = _make_overpass_settings()
391
+
392
+ # subdivide query polygon to get list of sub-divided polygon coord strings
393
+ polygon_coord_strs = _make_overpass_polygon_coord_strs(polygon)
394
+ msg = f"Requesting data from API in {len(polygon_coord_strs)} request(s)"
395
+ utils.log(msg, level=lg.INFO)
396
+
397
+ # pass exterior coordinates of each polygon in list to API, one at a time
398
+ # the '>' makes it recurse so we get ways and the ways' nodes.
399
+ for polygon_coord_str in polygon_coord_strs:
400
+ for way_filter in way_filters:
401
+ query_str = f"{overpass_settings};(way{way_filter}(poly:{polygon_coord_str!r});>;);out;"
402
+ yield _overpass_request(OrderedDict(data=query_str))
403
+
404
+
405
+ def _download_overpass_features(
406
+ polygon: Polygon,
407
+ tags: dict[str, bool | str | list[str]],
408
+ ) -> Iterator[dict[str, Any]]:
409
+ """
410
+ Retrieve OSM features within some boundary polygon from the Overpass API.
411
+
412
+ Parameters
413
+ ----------
414
+ polygon
415
+ Boundary to retrieve elements within.
416
+ tags
417
+ Tags used for finding elements in the selected area.
418
+
419
+ Yields
420
+ ------
421
+ response_json
422
+ JSON response from the Overpass server.
423
+ """
424
+ # subdivide query polygon to get list of sub-divided polygon coord strings
425
+ polygon_coord_strs = _make_overpass_polygon_coord_strs(polygon)
426
+ msg = f"Requesting data from API in {len(polygon_coord_strs)} request(s)"
427
+ utils.log(msg, level=lg.INFO)
428
+
429
+ # pass exterior coordinates of each polygon in list to API, one at a time
430
+ for polygon_coord_str in polygon_coord_strs:
431
+ query_str = _create_overpass_features_query(polygon_coord_str, tags)
432
+ yield _overpass_request(OrderedDict(data=query_str))
433
+
434
+
435
+ def _overpass_request(data: OrderedDict[str, Any]) -> dict[str, Any]:
436
+ """
437
+ Send a HTTP POST request to the Overpass API and return response.
438
+
439
+ Parameters
440
+ ----------
441
+ data
442
+ Key-value pairs of parameters.
443
+
444
+ Returns
445
+ -------
446
+ response_json
447
+ The Overpass API's response.
448
+ """
449
+ # resolve url to same IP even if there is server round-robin redirecting
450
+ _http._config_dns(settings.overpass_url)
451
+
452
+ # prepare the Overpass API URL and see if request already exists in cache
453
+ url = settings.overpass_url.rstrip("/") + "/interpreter"
454
+ prepared_url = str(requests.Request("GET", url, params=data).prepare().url)
455
+ cached_response_json = _http._retrieve_from_cache(prepared_url)
456
+ if isinstance(cached_response_json, dict):
457
+ return cached_response_json
458
+
459
+ # pause then request this URL
460
+ pause = _get_overpass_pause(settings.overpass_url)
461
+ hostname = _http._hostname_from_url(url)
462
+ msg = f"Pausing {pause} second(s) before making HTTP POST request to {hostname!r}"
463
+ utils.log(msg, level=lg.INFO)
464
+ time.sleep(pause)
465
+
466
+ # transmit the HTTP POST request
467
+ msg = f"Post {prepared_url} with timeout={settings.requests_timeout}"
468
+ utils.log(msg, level=lg.INFO)
469
+ response = requests.post(
470
+ url,
471
+ data=data,
472
+ timeout=settings.requests_timeout,
473
+ headers=_http._get_http_headers(),
474
+ **settings.requests_kwargs,
475
+ )
476
+
477
+ # handle 429 and 504 errors by pausing then recursively re-trying request
478
+ if response.status_code in {429, 504}: # pragma: no cover
479
+ error_pause = 55
480
+ msg = (
481
+ f"{hostname!r} responded {response.status_code} {response.reason}: "
482
+ f"we'll retry in {error_pause} secs"
483
+ )
484
+ utils.log(msg, level=lg.WARNING)
485
+ time.sleep(error_pause)
486
+ return _overpass_request(data)
487
+
488
+ response_json = _http._parse_response(response)
489
+ if not isinstance(response_json, dict): # pragma: no cover
490
+ msg = "Overpass API did not return a dict of results."
491
+ raise InsufficientResponseError(msg)
492
+ _http._save_to_cache(prepared_url, response_json, response.ok)
493
+ return response_json
osmnx/source/osmnx/_validate.py ADDED
@@ -0,0 +1,387 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Validate that graphs and GeoDataFrames satisfy OSMnx expectations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging as lg
6
+ from numbers import Real
7
+ from warnings import warn
8
+
9
+ import geopandas as gpd
10
+ import networkx as nx
11
+ import numpy as np
12
+
13
+ from ._errors import ValidationError
14
+ from .utils import log
15
+
16
+
17
+ def _verify_numeric_edge_attribute(G: nx.MultiDiGraph, attr: str, *, strict: bool = True) -> None:
18
+ """
19
+ Verify attribute values are numeric and non-null across graph edges.
20
+
21
+ Raises a ValidationError if this attribute contains non-numeric
22
+ values, and issues a UserWarning if this attribute is missing or null on
23
+ any edges.
24
+
25
+ Parameters
26
+ ----------
27
+ G
28
+ Input graph.
29
+ attr
30
+ Name of the edge attribute to verify.
31
+ strict
32
+ If `True`, elevate warnings to errors.
33
+ """
34
+ is_valid = True
35
+ valid_msg = "Verified {attr!r} values are numeric and non-null across graph edges."
36
+ warn_msg = ""
37
+ err_msg = ""
38
+
39
+ try:
40
+ values_float = (np.array(tuple(G.edges(data=attr)))[:, 2]).astype(float)
41
+ if np.isnan(values_float).any():
42
+ warn_msg += f"The attribute {attr!r} is missing or null on some edges."
43
+ if strict:
44
+ is_valid = False
45
+ except ValueError:
46
+ err_msg += f"The edge attribute {attr!r} contains non-numeric values."
47
+ is_valid = False
48
+
49
+ _report_validation(is_valid, valid_msg, warn_msg, err_msg)
50
+
51
+
52
+ def _validate_features_gdf(gdf: gpd.GeoDataFrame) -> None:
53
+ """
54
+ Validate that features GeoDataFrame satisfies OSMnx expectations.
55
+
56
+ Raises a `ValidationError` if validation fails.
57
+
58
+ Parameters
59
+ ----------
60
+ gdf
61
+ GeoDataFrame of features uniquely multi-indexed by
62
+ `(element_type, osmid)`.
63
+ """
64
+ is_valid = True
65
+ valid_msg = "Validated features GeoDataFrame."
66
+ warn_msg = ""
67
+ err_msg = ""
68
+
69
+ # ensure gdf is uniquely indexed
70
+ if not gdf.index.is_unique:
71
+ err_msg += "`gdf` must be uniquely indexed. "
72
+ is_valid = False
73
+
74
+ # ensure gdf is multi-indexed with 2 levels (element_type and osmid) and
75
+ # that the element types are all either node, way, or relation
76
+ features_index_levels = 2
77
+ check1 = gdf.index.nlevels == features_index_levels
78
+ element_types = set(gdf.index.get_level_values(0))
79
+ check2 = element_types.issubset({"node", "way", "relation"})
80
+ if not (check1 and check2):
81
+ err_msg += "`gdf` must be multi-indexed by `(element_type, osmid)`. "
82
+ is_valid = False
83
+
84
+ # ensure gdf has an active geometry column with all valid non-null geoms
85
+ if (gdf.active_geometry_name is None) or (
86
+ gdf.geometry.isna() | gdf.geometry.is_empty | ~gdf.geometry.is_valid
87
+ ).any():
88
+ err_msg += "`gdf` must contain valid, non-null geometries`. "
89
+ is_valid = False
90
+
91
+ _report_validation(is_valid, valid_msg, warn_msg, err_msg)
92
+
93
+
94
+ def _validate_node_edge_gdfs(
95
+ gdf_nodes: gpd.GeoDataFrame,
96
+ gdf_edges: gpd.GeoDataFrame,
97
+ *,
98
+ strict: bool = True,
99
+ ) -> None:
100
+ """
101
+ Validate that node/edge GeoDataFrames can be converted to a MultiDiGraph.
102
+
103
+ Raises a `ValidationError` if validation fails.
104
+
105
+ Parameters
106
+ ----------
107
+ gdf_nodes
108
+ GeoDataFrame of graph nodes uniquely indexed by `osmid`.
109
+ gdf_edges
110
+ GeoDataFrame of graph edges uniquely multi-indexed by `(u, v, key)`.
111
+ strict
112
+ If `True`, elevate warnings to errors.
113
+ """
114
+ is_valid = True
115
+ valid_msg = "Validated that node/edge GeoDataFrames can be converted to a MultiDiGraph."
116
+ warn_msg = ""
117
+ err_msg = ""
118
+
119
+ # ensure type is GeoDataFrame
120
+ if not (isinstance(gdf_nodes, gpd.GeoDataFrame) and isinstance(gdf_edges, gpd.GeoDataFrame)):
121
+ # if they are not both GeoDataFrames
122
+ err_msg += "`gdf_nodes` and `gdf_edges` must be GeoDataFrames. "
123
+ is_valid = False
124
+ # if they are both GeoDataFrames...
125
+ # warn user if geometry values differ from coordinates in x/y columns,
126
+ # because we ignore the geometry column
127
+ elif gdf_nodes.active_geometry_name is not None:
128
+ msg = (
129
+ "Will ignore the `gdf_nodes` 'geometry' column, though its values "
130
+ "differ from the coordinates in the 'x' and 'y' columns. "
131
+ )
132
+ try:
133
+ all_x_match = (gdf_nodes.geometry.x == gdf_nodes["x"]).all()
134
+ all_y_match = (gdf_nodes.geometry.y == gdf_nodes["y"]).all()
135
+ if not (all_x_match and all_y_match):
136
+ # warn if x/y coords don't match geometry column
137
+ warn_msg += msg
138
+ if strict:
139
+ is_valid = False
140
+ except ValueError:
141
+ # warn if geometry column contains non-point geometry types
142
+ warn_msg += msg
143
+ if strict:
144
+ is_valid = False
145
+
146
+ # ensure gdf_nodes has x and y columns representing node geometries
147
+ if not ("x" in gdf_nodes.columns and "y" in gdf_nodes.columns):
148
+ err_msg += "`gdf_nodes` must have 'x' and 'y' columns. "
149
+ is_valid = False
150
+
151
+ # ensure gdf_nodes and gdf_edges are uniquely indexed
152
+ if not (gdf_nodes.index.is_unique and gdf_edges.index.is_unique):
153
+ err_msg += "`gdf_nodes` and `gdf_edges` must each be uniquely indexed. "
154
+ is_valid = False
155
+
156
+ # ensure 1) gdf_edges are multi-indexed with 3 levels and 2) that its u
157
+ # and v values (first two index levels) all appear among gdf_nodes index
158
+ edges_index_levels = 3
159
+ check1 = gdf_edges.index.nlevels == edges_index_levels
160
+ try:
161
+ uv = set(gdf_edges.index.get_level_values(0)) | set(gdf_edges.index.get_level_values(1))
162
+ check2 = uv.issubset(set(gdf_nodes.index))
163
+ except IndexError:
164
+ check2 = False
165
+ if not (check1 and check2):
166
+ err_msg += "`gdf_edges` must be multi-indexed by `(u, v, key)`. "
167
+ is_valid = False
168
+
169
+ _report_validation(is_valid, valid_msg, warn_msg, err_msg)
170
+
171
+
172
+ def _validate_nodes(G: nx.MultiDiGraph, strict: bool) -> tuple[bool, str, str]: # noqa: FBT001
173
+ """
174
+ Validate that a graph's nodes satisfy OSMnx expectations.
175
+
176
+ Parameters
177
+ ----------
178
+ G
179
+ The input graph.
180
+ strict
181
+ If `True`, elevate warnings to errors.
182
+
183
+ Returns
184
+ -------
185
+ is_valid, err_msg, warn_msg
186
+ Whether validation passed, plus any error or warning messages.
187
+ """
188
+ # assume nodes are valid but try to falsify that through a series of tests
189
+ is_valid = True
190
+ err_msg = ""
191
+ warn_msg = ""
192
+
193
+ # ERR: must have at least 1 node
194
+ if not len(G.nodes) > 0:
195
+ err_msg += "G must have at least 1 node. "
196
+ is_valid = False
197
+
198
+ # otherwise, it has at least 1 node, so validate the node attributes
199
+ else:
200
+ # ERR: nodes must have "x" and "y" data attributes
201
+ if not all("x" in d and "y" in d for d in dict(G.nodes(data=True)).values()):
202
+ err_msg += "Nodes must have 'x' and 'y' data attributes. "
203
+ is_valid = False
204
+
205
+ # WARN: nodes' "x" and "y" data attributes should be type Real
206
+ valid_xs = all(isinstance(x, Real) for x in nx.get_node_attributes(G, name="x").values())
207
+ valid_ys = all(isinstance(y, Real) for y in nx.get_node_attributes(G, name="y").values())
208
+ if not (valid_xs and valid_ys):
209
+ warn_msg += "Node 'x' and 'y' data attributes should be numeric. "
210
+ if strict:
211
+ is_valid = False
212
+
213
+ # WARN: nodes should have "street_count" data attributes
214
+ if not all("street_count" in d for d in dict(G.nodes(data=True)).values()):
215
+ warn_msg += "Nodes should have 'street_count' data attributes. "
216
+ if strict:
217
+ is_valid = False
218
+
219
+ # WARN: nodes' "x" and "y" data attributes should be type Real
220
+ valid_xs = all(isinstance(x, Real) for x in nx.get_node_attributes(G, name="x").values())
221
+ valid_ys = all(isinstance(y, Real) for y in nx.get_node_attributes(G, name="y").values())
222
+ if not (valid_xs and valid_ys):
223
+ warn_msg += "Node 'x' and 'y' data attributes should be numeric. "
224
+ if strict:
225
+ is_valid = False
226
+
227
+ # WARN: node IDs should be type int
228
+ if not all(isinstance(n, int) for n in G.nodes):
229
+ warn_msg += "Node IDs should be type int. "
230
+ if strict:
231
+ is_valid = False
232
+
233
+ return is_valid, err_msg, warn_msg
234
+
235
+
236
+ def _validate_edges(G: nx.MultiDiGraph, strict: bool) -> tuple[bool, str, str]: # noqa: FBT001
237
+ """
238
+ Validate that a graph's edges satisfy OSMnx expectations.
239
+
240
+ Parameters
241
+ ----------
242
+ G
243
+ The input graph.
244
+ strict
245
+ If `True`, elevate warnings to errors.
246
+
247
+ Returns
248
+ -------
249
+ is_valid, err_msg, warn_msg
250
+ Whether validation passed, plus any error or warning messages.
251
+ """
252
+ # assume edges are valid but try to falsify that through a series of tests
253
+ is_valid = True
254
+ err_msg = ""
255
+ warn_msg = ""
256
+
257
+ # ERR: must have at least 1 edge
258
+ if not len(G.edges) > 0:
259
+ err_msg += "G must have at least 1 edge. "
260
+ is_valid = False
261
+
262
+ # otherwise, it has at least 1 edge, so validate the edge attributes
263
+ else:
264
+ # ERR: edges must have "osmid" data attributes
265
+ edge_osmids = nx.get_edge_attributes(G, name="osmid")
266
+ if set(edge_osmids) != set(G.edges):
267
+ err_msg += "Edges must have 'osmid' data attributes. "
268
+ is_valid = False
269
+
270
+ # WARN: edge "osmid" data attributes should be type int or list[int]
271
+ if not all(isinstance(x, (int, list)) for x in edge_osmids.values()):
272
+ warn_msg += "Edge 'osmid' data attributes should be type `int` or `list[int]`. "
273
+ if strict:
274
+ is_valid = False
275
+
276
+ # ERR: edges must have "length" data attributes
277
+ edge_lengths = nx.get_edge_attributes(G, name="length")
278
+ if set(edge_lengths) != set(G.edges):
279
+ err_msg += "Edges must have 'length' data attributes. "
280
+ is_valid = False
281
+
282
+ # WARN: edge "length" data attributes should be numeric
283
+ if not all(isinstance(x, Real) for x in edge_lengths.values()):
284
+ warn_msg += "Edge 'length' data attributes should be numeric. "
285
+ if strict:
286
+ is_valid = False
287
+
288
+ return is_valid, err_msg, warn_msg
289
+
290
+
291
+ def _validate_graph_attrs(G: nx.MultiDiGraph) -> tuple[bool, str, str]:
292
+ """
293
+ Validate that a graph's attributes satisfy OSMnx expectations.
294
+
295
+ Parameters
296
+ ----------
297
+ G
298
+ The input graph.
299
+
300
+ Returns
301
+ -------
302
+ is_valid, err_msg, warn_msg
303
+ Whether validation passed, plus any error or warning messages.
304
+ """
305
+ # assume G is valid but try to falsify that through a series of tests
306
+ is_valid = True
307
+ err_msg = ""
308
+ warn_msg = ""
309
+
310
+ # ERR: must be a NetworkX MultiDiGraph
311
+ if not isinstance(G, nx.MultiDiGraph):
312
+ err_msg += "G must be a NetworkX MultiDiGraph. "
313
+ is_valid = False
314
+
315
+ # ERR: must have top-level graph, nodes, and edges attributes
316
+ if not (hasattr(G, "graph") and hasattr(G, "nodes") and hasattr(G, "edges")):
317
+ err_msg += "G must have top-level graph, nodes, and edges attributes. "
318
+ is_valid = False
319
+
320
+ # ERR: graph attr dict must have a "crs" key defining its CRS
321
+ crs = getattr(G, "graph", {}).get("crs")
322
+ if crs is None:
323
+ err_msg += "G.graph must have a 'crs' data attribute. "
324
+ is_valid = False
325
+
326
+ # ERR: graph attr dict "crs" value must be a valid pyproj CRS
327
+ else:
328
+ try:
329
+ _ = gpd.GeoSeries(crs=crs).crs
330
+ except RuntimeError: # RuntimeError is parent of pyproj CRSError
331
+ err_msg += "G.graph['crs'] must be a valid CRS. "
332
+ is_valid = False
333
+
334
+ return is_valid, err_msg, warn_msg
335
+
336
+
337
+ def _validate_graph(G: nx.MultiDiGraph, *, strict: bool = True) -> None:
338
+ """
339
+ Validate that a graph object satisfies OSMnx expectations.
340
+
341
+ Raises `ox._errors.ValidationError` if validation fails.
342
+
343
+ Parameters
344
+ ----------
345
+ G
346
+ The input graph.
347
+ strict
348
+ If `True`, elevate warnings to errors.
349
+ """
350
+ # validate graph, nodes, and edges
351
+ is_valid_graph, err_msg_graph, warn_msg_graph = _validate_graph_attrs(G)
352
+ is_valid_nodes, err_msg_nodes, warn_msg_nodes = _validate_nodes(G, strict)
353
+ is_valid_edges, err_msg_edges, warn_msg_edges = _validate_edges(G, strict)
354
+
355
+ # report results
356
+ is_valid = is_valid_graph and is_valid_nodes and is_valid_edges
357
+ err_msg = err_msg_graph + err_msg_nodes + err_msg_edges
358
+ warn_msg = warn_msg_graph + warn_msg_nodes + warn_msg_edges
359
+ valid_msg = "Successfully validated graph."
360
+ _report_validation(is_valid, valid_msg, warn_msg, err_msg)
361
+
362
+
363
+ def _report_validation(is_valid: bool, valid_msg: str, warn_msg: str, err_msg: str) -> None: # noqa: FBT001
364
+ """
365
+ Report validation results by logging, warning, or raising an exception.
366
+
367
+ Parameters
368
+ ----------
369
+ is_valid
370
+ Whether or not the validation succeeded.
371
+ valid_msg
372
+ The message to log if validation succeeded.
373
+ warn_msg
374
+ Any warning messages to log and either issue a warning or include in
375
+ error message.
376
+ err_msg
377
+ Any error messages to include when raising exception if validation
378
+ failed.
379
+ """
380
+ if is_valid:
381
+ log(valid_msg, level=lg.INFO)
382
+ if warn_msg != "":
383
+ log(warn_msg, level=lg.WARNING)
384
+ warn(warn_msg, category=UserWarning, stacklevel=2)
385
+ else:
386
+ log(err_msg + warn_msg, level=lg.ERROR)
387
+ raise ValidationError(err_msg + warn_msg)
osmnx/source/osmnx/bearing.py ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Calculate graph edge bearings and orientation entropy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from types import ModuleType
6
+ from typing import TYPE_CHECKING
7
+ from typing import overload
8
+ from warnings import warn
9
+
10
+ import networkx as nx
11
+ import numpy as np
12
+ import numpy.typing as npt
13
+
14
+ from . import projection
15
+
16
+ if TYPE_CHECKING:
17
+ from types import ModuleType
18
+
19
+ # scipy is an optional dependency for entropy calculation
20
+ scipy: ModuleType | None
21
+ try:
22
+ import scipy
23
+ except ImportError: # pragma: no cover
24
+ scipy = None
25
+
26
+
27
+ # if coords are all floats, return float
28
+ @overload
29
+ def calculate_bearing(
30
+ lat1: float,
31
+ lon1: float,
32
+ lat2: float,
33
+ lon2: float,
34
+ ) -> float: ...
35
+
36
+
37
+ # if coords are all arrays, return array
38
+ @overload
39
+ def calculate_bearing(
40
+ lat1: npt.NDArray[np.float64],
41
+ lon1: npt.NDArray[np.float64],
42
+ lat2: npt.NDArray[np.float64],
43
+ lon2: npt.NDArray[np.float64],
44
+ ) -> npt.NDArray[np.float64]: ...
45
+
46
+
47
+ def calculate_bearing(
48
+ lat1: float | npt.NDArray[np.float64],
49
+ lon1: float | npt.NDArray[np.float64],
50
+ lat2: float | npt.NDArray[np.float64],
51
+ lon2: float | npt.NDArray[np.float64],
52
+ ) -> float | npt.NDArray[np.float64]:
53
+ """
54
+ Calculate the compass bearing(s) between pairs of lat-lon points.
55
+
56
+ Vectorized function to calculate initial bearings between two points'
57
+ coordinates or between arrays of points' coordinates. Expects coordinates
58
+ in decimal degrees. The bearing represents the clockwise angle in degrees
59
+ between north and the geodesic line from `(lat1, lon1)` to `(lat2, lon2)`.
60
+
61
+ Parameters
62
+ ----------
63
+ lat1
64
+ First point's latitude coordinate(s).
65
+ lon1
66
+ First point's longitude coordinate(s).
67
+ lat2
68
+ Second point's latitude coordinate(s).
69
+ lon2
70
+ Second point's longitude coordinate(s).
71
+
72
+ Returns
73
+ -------
74
+ bearing
75
+ The bearing(s) in decimal degrees.
76
+ """
77
+ # get the latitudes and the difference in longitudes, all in radians
78
+ lat1 = np.deg2rad(lat1)
79
+ lat2 = np.deg2rad(lat2)
80
+ delta_lon = np.deg2rad(lon2 - lon1)
81
+
82
+ # calculate initial bearing from -180 degrees to +180 degrees
83
+ y = np.sin(delta_lon) * np.cos(lat2)
84
+ x = np.cos(lat1) * np.sin(lat2) - np.sin(lat1) * np.cos(lat2) * np.cos(delta_lon)
85
+ initial_bearing = np.rad2deg(np.arctan2(y, x))
86
+
87
+ # normalize to 0-360 degrees to get compass bearing
88
+ bearing: float | npt.NDArray[np.float64] = initial_bearing % 360
89
+ return bearing
90
+
91
+
92
+ def add_edge_bearings(G: nx.MultiDiGraph) -> nx.MultiDiGraph:
93
+ """
94
+ Calculate and add compass `bearing` attributes to all graph edges.
95
+
96
+ Vectorized function to calculate (initial) bearing from origin node to
97
+ destination node for each edge in a directed, unprojected graph then add
98
+ these bearings as new `bearing` edge attributes. Bearing represents angle
99
+ in degrees (clockwise) between north and the geodesic line from the origin
100
+ node to the destination node. Ignores self-loop edges as their bearings
101
+ are undefined.
102
+
103
+ Parameters
104
+ ----------
105
+ G
106
+ Unprojected graph.
107
+
108
+ Returns
109
+ -------
110
+ G
111
+ Graph with `bearing` attributes on the edges.
112
+ """
113
+ if projection.is_projected(G.graph["crs"]): # pragma: no cover
114
+ msg = "Graph must be unprojected to add edge bearings."
115
+ raise ValueError(msg)
116
+
117
+ # extract edge IDs and corresponding coordinates from their nodes
118
+ uvk = [(u, v, k) for u, v, k in G.edges if u != v]
119
+ x = G.nodes(data="x")
120
+ y = G.nodes(data="y")
121
+ coords = np.array([(y[u], x[u], y[v], x[v]) for u, v, k in uvk])
122
+
123
+ # calculate bearings then set as edge attributes
124
+ bearings = calculate_bearing(coords[:, 0], coords[:, 1], coords[:, 2], coords[:, 3])
125
+ values = zip(uvk, bearings, strict=True)
126
+ nx.set_edge_attributes(G, dict(values), name="bearing")
127
+
128
+ return G
129
+
130
+
131
+ def orientation_entropy(
132
+ G: nx.MultiGraph | nx.MultiDiGraph,
133
+ *,
134
+ num_bins: int = 36,
135
+ min_length: float = 0,
136
+ weight: str | None = None,
137
+ ) -> float:
138
+ """
139
+ Calculate graph's orientation entropy.
140
+
141
+ Orientation entropy is the Shannon entropy of the graphs' edges' bearings
142
+ across evenly spaced bins. Ignores self-loop edges as their bearings are
143
+ undefined. If `G` is a MultiGraph, all edge bearings will be bidirectional
144
+ (ie, two reciprocal bearings per undirected edge). If `G` is a
145
+ MultiDiGraph, all edge bearings will be directional (ie, one bearing per
146
+ directed edge).
147
+
148
+ For more info see: Boeing, G. 2019. "Urban Spatial Order: Street Network
149
+ Orientation, Configuration, and Entropy." Applied Network Science, 4 (1),
150
+ 67. https://doi.org/10.1007/s41109-019-0189-1
151
+
152
+ Parameters
153
+ ----------
154
+ G
155
+ Unprojected graph with `bearing` attributes on each edge.
156
+ num_bins
157
+ Number of bins. For example, if `num_bins=36` is provided, then each
158
+ bin will represent 10 degrees around the compass.
159
+ min_length
160
+ Ignore edges with "length" attributes less than `min_length`. Useful
161
+ to ignore the noise of many very short edges.
162
+ weight
163
+ If None, apply equal weight for each bearing. Otherwise, weight edges'
164
+ bearings by this (non-null) edge attribute. For example, if "length"
165
+ is provided, each edge's bearing observation will be weighted by its
166
+ "length" attribute value.
167
+
168
+ Returns
169
+ -------
170
+ entropy
171
+ The orientation entropy of `G`.
172
+ """
173
+ # check if we were able to import scipy
174
+ if scipy is None: # pragma: no cover
175
+ msg = "scipy must be installed as an optional dependency to calculate entropy."
176
+ raise ImportError(msg)
177
+ bin_counts, _ = _bearings_distribution(G, num_bins, min_length, weight)
178
+ entropy: float = scipy.stats.entropy(bin_counts)
179
+ return entropy
180
+
181
+
182
+ def _extract_edge_bearings(
183
+ G: nx.MultiGraph | nx.MultiDiGraph,
184
+ min_length: float,
185
+ weight: str | None,
186
+ ) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
187
+ """
188
+ Extract graph's edge bearings.
189
+
190
+ Ignores self-loop edges as their bearings are undefined. If `G` is a
191
+ MultiGraph, all edge bearings will be bidirectional (ie, two reciprocal
192
+ bearings per undirected edge). If `G` is a MultiDiGraph, all edge bearings
193
+ will be directional (ie, one bearing per directed edge). For example, if
194
+ an undirected edge has a bearing of 90 degrees then we will record
195
+ bearings of both 90 degrees and 270 degrees for this edge.
196
+
197
+ Parameters
198
+ ----------
199
+ G
200
+ Unprojected graph with `bearing` attributes on each edge.
201
+ min_length
202
+ Ignore edges with `length` attributes less than `min_length`. Useful
203
+ to ignore the noise of many very short edges.
204
+ weight
205
+ If None, apply equal weight for each bearing. Otherwise, weight edges'
206
+ bearings by this (non-null) edge attribute. For example, if "length"
207
+ is provided, each edge's bearing observation will be weighted by its
208
+ "length" attribute value.
209
+
210
+ Returns
211
+ -------
212
+ bearings, weights
213
+ The edge bearings of `G` and their corresponding weights.
214
+ """
215
+ if projection.is_projected(G.graph["crs"]): # pragma: no cover
216
+ msg = "Graph must be unprojected to analyze edge bearings."
217
+ raise ValueError(msg)
218
+ bearings = []
219
+ weights = []
220
+ for u, v, data in G.edges(data=True):
221
+ # ignore self-loops and any edges below min_length
222
+ if u != v and data["length"] >= min_length:
223
+ bearings.append(data["bearing"])
224
+ weights.append(data[weight] if weight is not None else 1.0)
225
+
226
+ # drop any nulls
227
+ bearings_array = np.array(bearings)
228
+ weights_array = np.array(weights)
229
+ keep_idx = ~np.isnan(bearings_array)
230
+ bearings_array = bearings_array[keep_idx]
231
+ weights_array = weights_array[keep_idx]
232
+ if nx.is_directed(G):
233
+ msg = (
234
+ "`G` is a MultiDiGraph, so edge bearings will be directional (one per "
235
+ "edge). If you want bidirectional edge bearings (two reciprocal bearings "
236
+ "per edge), pass a MultiGraph instead. Use `convert.to_undirected`."
237
+ )
238
+ warn(msg, category=UserWarning, stacklevel=2)
239
+ return bearings_array, weights_array
240
+ # for undirected graphs, add reverse bearings
241
+ bearings_array = np.concatenate([bearings_array, (bearings_array - 180) % 360])
242
+ weights_array = np.concatenate([weights_array, weights_array])
243
+ return bearings_array, weights_array
244
+
245
+
246
+ def _bearings_distribution(
247
+ G: nx.MultiGraph | nx.MultiDiGraph,
248
+ num_bins: int,
249
+ min_length: float,
250
+ weight: str | None,
251
+ ) -> tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]]:
252
+ """
253
+ Compute distribution of bearings across evenly spaced bins.
254
+
255
+ Prevents bin-edge effects around common values like 0 degrees and 90
256
+ degrees by initially creating twice as many bins as desired, then merging
257
+ them in pairs. For example, if `num_bins=36` is provided, then each bin
258
+ will represent 10 degrees around the compass, with the first bin
259
+ representing 355 degrees to 5 degrees.
260
+
261
+ Parameters
262
+ ----------
263
+ G
264
+ Unprojected graph with `bearing` attributes on each edge.
265
+ num_bins
266
+ Number of bins for the bearing histogram.
267
+ min_length
268
+ Ignore edges with `length` attributes less than `min_length`. Useful
269
+ to ignore the noise of many very short edges.
270
+ weight
271
+ If None, apply equal weight for each bearing. Otherwise, weight edges'
272
+ bearings by this (non-null) edge attribute. For example, if "length"
273
+ is provided, each edge's bearing observation will be weighted by its
274
+ "length" attribute value.
275
+
276
+ Returns
277
+ -------
278
+ bin_counts, bin_centers
279
+ Counts of bearings per bin and the bins' centers in degrees. Both
280
+ arrays are of length `num_bins`.
281
+ """
282
+ # Split bins in half to prevent bin-edge effects around common values.
283
+ # Bins will be merged in pairs after the histogram is computed. The last
284
+ # bin edge is the same as the first (i.e., 0 degrees = 360 degrees).
285
+ num_split_bins = num_bins * 2
286
+ split_bin_edges = np.linspace(0, 360, num_split_bins + 1)
287
+
288
+ bearings, weights = _extract_edge_bearings(G, min_length, weight)
289
+ split_bin_counts, split_bin_edges = np.histogram(
290
+ bearings,
291
+ bins=split_bin_edges,
292
+ weights=weights,
293
+ )
294
+
295
+ # Move last bin to front, so eg 0.01 degrees and 359.99 degrees will be
296
+ # binned together. Then combine counts from pairs of split bins.
297
+ split_bin_counts = np.roll(split_bin_counts, 1)
298
+ bin_counts = split_bin_counts[::2] + split_bin_counts[1::2]
299
+
300
+ # Every other edge of the split bins is the center of a merged bin.
301
+ bin_centers = split_bin_edges[range(0, num_split_bins - 1, 2)]
302
+ return bin_counts, bin_centers
osmnx/source/osmnx/convert.py ADDED
@@ -0,0 +1,564 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Convert spatial graphs to/from different data types."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import itertools
6
+ import logging as lg
7
+ from typing import Any
8
+ from typing import Literal
9
+ from typing import overload
10
+
11
+ import geopandas as gpd
12
+ import networkx as nx
13
+ import pandas as pd
14
+ from shapely import LineString
15
+ from shapely import Point
16
+
17
+ from . import _validate
18
+ from . import utils
19
+
20
+
21
+ def validate_graph(G: nx.MultiDiGraph, *, strict: bool = True) -> None:
22
+ """
23
+ Validate that a graph object satisfies OSMnx expectations.
24
+
25
+ Raises `ox._errors.GraphValidationError` if validation fails.
26
+
27
+ Parameters
28
+ ----------
29
+ G
30
+ The input graph.
31
+ strict
32
+ If `True`, enforce optional rules in addition to required rules. These
33
+ optional rules primarily enforce expected attribute data types.
34
+ """
35
+ _validate._validate_graph(G, strict=strict)
36
+
37
+
38
+ def validate_node_edge_gdfs(
39
+ gdf_nodes: gpd.GeoDataFrame,
40
+ gdf_edges: gpd.GeoDataFrame,
41
+ *,
42
+ strict: bool = True,
43
+ ) -> None:
44
+ """
45
+ Validate that node/edge GeoDataFrames can be converted to a MultiDiGraph.
46
+
47
+ Raises a `ValidationError` if validation fails.
48
+
49
+ Parameters
50
+ ----------
51
+ gdf_nodes
52
+ GeoDataFrame of graph nodes uniquely indexed by `osmid`.
53
+ gdf_edges
54
+ GeoDataFrame of graph edges uniquely multi-indexed by `(u, v, key)`.
55
+ strict
56
+ If `True`, elevate warnings to errors.
57
+ """
58
+ _validate._validate_node_edge_gdfs(gdf_nodes, gdf_edges, strict=strict)
59
+
60
+
61
+ def validate_features_gdf(gdf: gpd.GeoDataFrame) -> None:
62
+ """
63
+ Validate that features GeoDataFrame satisfies OSMnx expectations.
64
+
65
+ Raises a `ValidationError` if validation fails.
66
+
67
+ Parameters
68
+ ----------
69
+ gdf
70
+ GeoDataFrame of features uniquely multi-indexed by
71
+ `(element_type, osmid)`.
72
+ """
73
+ _validate._validate_features_gdf(gdf)
74
+
75
+
76
+ # nodes and edges are both missing (therefore both default true)
77
+ @overload
78
+ def graph_to_gdfs(
79
+ G: nx.MultiGraph | nx.MultiDiGraph,
80
+ *,
81
+ node_geometry: bool = True,
82
+ fill_edge_geometry: bool = True,
83
+ ) -> tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]: ...
84
+
85
+
86
+ # both present/True
87
+ @overload
88
+ def graph_to_gdfs(
89
+ G: nx.MultiGraph | nx.MultiDiGraph,
90
+ *,
91
+ nodes: Literal[True],
92
+ edges: Literal[True],
93
+ node_geometry: bool = True,
94
+ fill_edge_geometry: bool = True,
95
+ ) -> tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]: ...
96
+
97
+
98
+ # both present, nodes true, edges false
99
+ @overload
100
+ def graph_to_gdfs(
101
+ G: nx.MultiGraph | nx.MultiDiGraph,
102
+ *,
103
+ nodes: Literal[True],
104
+ edges: Literal[False],
105
+ node_geometry: bool = True,
106
+ fill_edge_geometry: bool = True,
107
+ ) -> gpd.GeoDataFrame: ...
108
+
109
+
110
+ # both present, nodes false, edges true
111
+ @overload
112
+ def graph_to_gdfs(
113
+ G: nx.MultiGraph | nx.MultiDiGraph,
114
+ *,
115
+ nodes: Literal[False],
116
+ edges: Literal[True],
117
+ node_geometry: bool = True,
118
+ fill_edge_geometry: bool = True,
119
+ ) -> gpd.GeoDataFrame: ...
120
+
121
+
122
+ # nodes missing (therefore default true), edges present/true
123
+ @overload
124
+ def graph_to_gdfs(
125
+ G: nx.MultiGraph | nx.MultiDiGraph,
126
+ *,
127
+ edges: Literal[True],
128
+ node_geometry: bool = True,
129
+ fill_edge_geometry: bool = True,
130
+ ) -> tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]: ...
131
+
132
+
133
+ # nodes missing (therefore default true), edges present/false
134
+ @overload
135
+ def graph_to_gdfs(
136
+ G: nx.MultiGraph | nx.MultiDiGraph,
137
+ *,
138
+ edges: Literal[False],
139
+ node_geometry: bool = True,
140
+ fill_edge_geometry: bool = True,
141
+ ) -> gpd.GeoDataFrame: ...
142
+
143
+
144
+ # nodes present/true, edges missing (therefore default true)
145
+ @overload
146
+ def graph_to_gdfs(
147
+ G: nx.MultiGraph | nx.MultiDiGraph,
148
+ *,
149
+ nodes: Literal[True],
150
+ edges: bool = True,
151
+ node_geometry: bool = True,
152
+ fill_edge_geometry: bool = True,
153
+ ) -> tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]: ...
154
+
155
+
156
+ # nodes present/false, edges missing (therefore default true)
157
+ @overload
158
+ def graph_to_gdfs(
159
+ G: nx.MultiGraph | nx.MultiDiGraph,
160
+ *,
161
+ nodes: Literal[False],
162
+ edges: bool = True,
163
+ node_geometry: bool = True,
164
+ fill_edge_geometry: bool = True,
165
+ ) -> gpd.GeoDataFrame: ...
166
+
167
+
168
+ def graph_to_gdfs(
169
+ G: nx.MultiGraph | nx.MultiDiGraph,
170
+ *,
171
+ nodes: bool = True,
172
+ edges: bool = True,
173
+ node_geometry: bool = True,
174
+ fill_edge_geometry: bool = True,
175
+ ) -> gpd.GeoDataFrame | tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]:
176
+ """
177
+ Convert a MultiGraph or MultiDiGraph to node and/or edge GeoDataFrames.
178
+
179
+ This function is the inverse of `graph_from_gdfs`.
180
+
181
+ Parameters
182
+ ----------
183
+ G
184
+ Input graph.
185
+ nodes
186
+ If True, convert graph nodes to a GeoDataFrame and return it.
187
+ edges
188
+ If True, convert graph edges to a GeoDataFrame and return it.
189
+ node_geometry
190
+ If True, create a geometry column from node "x" and "y" attributes.
191
+ fill_edge_geometry
192
+ If True, fill missing edge geometry fields using endpoint nodes'
193
+ coordinates to create a LineString.
194
+
195
+ Returns
196
+ -------
197
+ gdf_nodes or gdf_edges or (gdf_nodes, gdf_edges)
198
+ `gdf_nodes` is indexed by `osmid` and `gdf_edges` is multi-indexed by
199
+ `(u, v, key)` following normal MultiGraph/MultiDiGraph structure.
200
+ """
201
+ crs = G.graph["crs"]
202
+
203
+ if nodes:
204
+ if len(G.nodes) == 0: # pragma: no cover
205
+ msg = "Graph contains no nodes."
206
+ raise ValueError(msg)
207
+
208
+ uvk, data = zip(*G.nodes(data=True), strict=True)
209
+
210
+ if node_geometry:
211
+ # convert node x/y attributes to Points for geometry column
212
+ node_geoms = (Point(d["x"], d["y"]) for d in data)
213
+ gdf_nodes = gpd.GeoDataFrame(data, index=uvk, crs=crs, geometry=list(node_geoms))
214
+ else:
215
+ gdf_nodes = gpd.GeoDataFrame(data, index=uvk)
216
+
217
+ gdf_nodes.index = gdf_nodes.index.rename("osmid")
218
+ msg = "Created nodes GeoDataFrame from graph"
219
+ utils.log(msg, level=lg.INFO)
220
+
221
+ if edges:
222
+ if len(G.edges) == 0: # pragma: no cover
223
+ msg = "Graph contains no edges."
224
+ raise ValueError(msg)
225
+
226
+ u, v, k, data = zip(*G.edges(keys=True, data=True), strict=True)
227
+
228
+ if fill_edge_geometry:
229
+ node_coords = {n: (G.nodes[n]["x"], G.nodes[n]["y"]) for n in G}
230
+ edge_geoms = (
231
+ d.get("geometry", LineString((node_coords[u], node_coords[v])))
232
+ for u, v, _, d in G.edges(keys=True, data=True)
233
+ )
234
+ gdf_edges = gpd.GeoDataFrame(data, crs=crs, geometry=list(edge_geoms))
235
+
236
+ else:
237
+ gdf_edges = gpd.GeoDataFrame(data)
238
+ if "geometry" not in gdf_edges.columns:
239
+ # if no edges have a geometry attribute, create null column
240
+ gdf_edges = gdf_edges.set_geometry([None] * len(gdf_edges))
241
+ gdf_edges = gdf_edges.set_crs(crs)
242
+
243
+ # add u, v, key attributes as index
244
+ gdf_edges["u"] = u
245
+ gdf_edges["v"] = v
246
+ gdf_edges["key"] = k
247
+ gdf_edges = gdf_edges.set_index(["u", "v", "key"])
248
+
249
+ msg = "Created edges GeoDataFrame from graph"
250
+ utils.log(msg, level=lg.INFO)
251
+
252
+ if nodes and edges:
253
+ return gdf_nodes, gdf_edges
254
+
255
+ if nodes:
256
+ return gdf_nodes
257
+
258
+ if edges:
259
+ return gdf_edges
260
+
261
+ # otherwise
262
+ msg = "You must request nodes or edges or both."
263
+ raise ValueError(msg)
264
+
265
+
266
+ def graph_from_gdfs(
267
+ gdf_nodes: gpd.GeoDataFrame,
268
+ gdf_edges: gpd.GeoDataFrame,
269
+ *,
270
+ graph_attrs: dict[str, Any] | None = None,
271
+ ) -> nx.MultiDiGraph:
272
+ """
273
+ Convert node and edge GeoDataFrames to a MultiDiGraph.
274
+
275
+ This function is the inverse of `graph_to_gdfs` and is designed to work in
276
+ conjunction with it. However, you can convert arbitrary node and edge
277
+ GeoDataFrames as long as 1) `gdf_nodes` is uniquely indexed by `osmid`, 2)
278
+ `gdf_nodes` contains `x` and `y` coordinate columns representing node
279
+ geometries, 3) `gdf_edges` is uniquely multi-indexed by `(u, v, key)`
280
+ (following normal MultiDiGraph structure). This allows you to load any
281
+ node/edge Shapefiles or GeoPackage layers as GeoDataFrames then convert
282
+ them to a MultiDiGraph for network analysis.
283
+
284
+ Note that any `geometry` attribute on `gdf_nodes` is discarded, since `x`
285
+ and `y` provide the necessary node geometry information instead.
286
+
287
+ Parameters
288
+ ----------
289
+ gdf_nodes
290
+ GeoDataFrame of graph nodes uniquely indexed by `osmid`.
291
+ gdf_edges
292
+ GeoDataFrame of graph edges uniquely multi-indexed by `(u, v, key)`.
293
+ graph_attrs
294
+ The new `G.graph` attribute dictionary. If None, use `gdf_edges`'s CRS
295
+ as the only graph-level attribute (`gdf_edges` must have its `crs`
296
+ attribute set).
297
+
298
+ Returns
299
+ -------
300
+ G
301
+ The converted MultiDiGraph.
302
+ """
303
+ validate_node_edge_gdfs(gdf_nodes, gdf_edges)
304
+
305
+ # drop geometry column from gdf_nodes (since we use x and y for geometry
306
+ # information), but warn the user if the geometry values differ from the
307
+ # coordinates in the x and y columns. this results in a df instead of gdf.
308
+ if gdf_nodes.active_geometry_name is None: # pragma: no cover
309
+ df_nodes = pd.DataFrame(gdf_nodes)
310
+ else:
311
+ df_nodes = gdf_nodes.drop(columns=gdf_nodes.active_geometry_name)
312
+
313
+ # create graph and add graph-level attribute dict
314
+ if graph_attrs is None:
315
+ graph_attrs = {"crs": gdf_edges.crs}
316
+ G = nx.MultiDiGraph(**graph_attrs)
317
+
318
+ # add edges and their attributes to graph, but filter out null attribute
319
+ # values so that edges only get attributes with non-null values
320
+ attr_names = gdf_edges.columns.to_list()
321
+ for (u, v, k), attr_vals in zip(gdf_edges.index, gdf_edges.to_numpy(), strict=True):
322
+ data_all = zip(attr_names, attr_vals, strict=True)
323
+ data = {name: val for name, val in data_all if isinstance(val, list) or pd.notna(val)}
324
+ G.add_edge(u, v, key=k, **data)
325
+
326
+ # add any nodes with no incident edges, since they wouldn't be added above
327
+ G.add_nodes_from(set(df_nodes.index) - set(G.nodes))
328
+
329
+ # now all nodes are added, so set nodes' attributes
330
+ for col in df_nodes.columns:
331
+ nx.set_node_attributes(G, name=col, values=df_nodes[col].dropna())
332
+
333
+ msg = "Created graph from node/edge GeoDataFrames"
334
+ utils.log(msg, level=lg.INFO)
335
+ return G
336
+
337
+
338
+ def to_digraph(G: nx.MultiDiGraph, *, weight: str = "length") -> nx.DiGraph:
339
+ """
340
+ Convert MultiDiGraph to DiGraph.
341
+
342
+ Chooses between parallel edges by minimizing `weight` attribute value. See
343
+ also `to_undirected` to convert MultiDiGraph to MultiGraph.
344
+
345
+ Parameters
346
+ ----------
347
+ G
348
+ Input graph.
349
+ weight
350
+ Attribute value to minimize when choosing between parallel edges.
351
+
352
+ Returns
353
+ -------
354
+ D
355
+ The converted DiGraph.
356
+ """
357
+ # make a copy to not mutate original graph object caller passed in
358
+ G = G.copy()
359
+ to_remove: list[tuple[int, int, int]] = []
360
+
361
+ # identify all the parallel edges in the MultiDiGraph
362
+ parallels = ((u, v) for u, v in G.edges(keys=False) if G.number_of_edges(u, v) > 1)
363
+
364
+ # among all sets of parallel edges, remove all except the one with the
365
+ # minimum "weight" attribute value
366
+ for u, v in set(parallels):
367
+ k_min, _ = min(G.get_edge_data(u, v).items(), key=lambda x: x[1][weight])
368
+ to_remove.extend((u, v, k) for k in G[u][v] if k != k_min)
369
+
370
+ G.remove_edges_from(to_remove)
371
+ msg = "Converted MultiDiGraph to DiGraph"
372
+ utils.log(msg, level=lg.INFO)
373
+
374
+ return nx.DiGraph(G)
375
+
376
+
377
+ def to_undirected(G: nx.MultiDiGraph) -> nx.MultiGraph:
378
+ """
379
+ Convert MultiDiGraph to undirected MultiGraph.
380
+
381
+ This function has a limited use case: it allows you to create a MultiGraph
382
+ for use with functions/algorithms that only accept a MultiGraph object.
383
+ Rather, if you want a fully bidirectional graph (such as for a walking
384
+ network), configure the `settings` module's `bidirectional_network_types`
385
+ before creating your graph to generate a fully bidirectional MultiDiGraph.
386
+
387
+ This function maintains parallel edges only if their geometries differ.
388
+ See also `to_digraph` to convert MultiDiGraph to DiGraph.
389
+
390
+ Parameters
391
+ ----------
392
+ G
393
+ Input graph.
394
+
395
+ Returns
396
+ -------
397
+ Gu
398
+ The converted MultiGraph.
399
+ """
400
+ # make a copy to not mutate original graph object caller passed in
401
+ G = G.copy()
402
+
403
+ # set from/to nodes before making graph undirected
404
+ for u, v, d in G.edges(data=True):
405
+ d["from"] = u
406
+ d["to"] = v
407
+
408
+ # add geometry if missing, to compare parallel edges' geometries
409
+ if "geometry" not in d:
410
+ point_u = (G.nodes[u]["x"], G.nodes[u]["y"])
411
+ point_v = (G.nodes[v]["x"], G.nodes[v]["y"])
412
+ d["geometry"] = LineString([point_u, point_v])
413
+
414
+ # increment parallel edges' keys so we don't retain only one edge of sets
415
+ # of true parallel edges when we convert from MultiDiGraph to MultiGraph
416
+ G = _update_edge_keys(G)
417
+
418
+ # convert MultiDiGraph to MultiGraph, retaining edges in both directions
419
+ # of parallel edges and self-loops for now
420
+ Gu = nx.MultiGraph(**G.graph)
421
+ Gu.add_nodes_from(G.nodes(data=True))
422
+ Gu.add_edges_from(G.edges(keys=True, data=True))
423
+
424
+ # the previous operation added all directed edges from G as undirected
425
+ # edges in Gu. we now have duplicate edges for each bidirectional parallel
426
+ # edge or self-loop. so, look through the edges and remove any duplicates.
427
+ duplicate_edges = set()
428
+ for u1, v1, key1, data1 in Gu.edges(keys=True, data=True):
429
+ # if we haven't already flagged this edge as a duplicate
430
+ if (u1, v1, key1) not in duplicate_edges:
431
+ # look at every other edge between u and v, one at a time
432
+ for key2 in Gu[u1][v1]:
433
+ # don't compare this edge to itself
434
+ if key1 != key2:
435
+ # compare the first edge's data to the second's
436
+ # if they match up, flag the duplicate for removal
437
+ data2 = Gu.edges[u1, v1, key2]
438
+ if _is_duplicate_edge(data1, data2):
439
+ duplicate_edges.add((u1, v1, key2))
440
+
441
+ Gu.remove_edges_from(duplicate_edges)
442
+ msg = "Converted MultiDiGraph to undirected MultiGraph"
443
+ utils.log(msg, level=lg.INFO)
444
+
445
+ return Gu
446
+
447
+
448
+ def _is_duplicate_edge(data1: dict[str, Any], data2: dict[str, Any]) -> bool:
449
+ """
450
+ Check if two graph edge data dicts have the same `osmid` and `geometry`.
451
+
452
+ Parameters
453
+ ----------
454
+ data1
455
+ The first edge's attribute data.
456
+ data2
457
+ The second edge's attribute data.
458
+
459
+ Returns
460
+ -------
461
+ is_dupe
462
+ True if `osmid` and `geometry` are the same, otherwise False.
463
+ """
464
+ is_dupe = False
465
+
466
+ # if either edge's osmid contains multiple values (due to simplification)
467
+ # compare them as sets to see if they contain the same values
468
+ osmid1 = set(data1["osmid"]) if isinstance(data1["osmid"], list) else data1["osmid"]
469
+ osmid2 = set(data2["osmid"]) if isinstance(data2["osmid"], list) else data2["osmid"]
470
+
471
+ # if they contain the same osmid or set of osmids (due to simplification)
472
+ if osmid1 == osmid2:
473
+ # if both edges have geometry attributes and they match each other
474
+ if ("geometry" in data1) and ("geometry" in data2):
475
+ if _is_same_geometry(data1["geometry"], data2["geometry"]):
476
+ is_dupe = True
477
+
478
+ # if neither edge has a geometry attribute
479
+ elif ("geometry" not in data1) and ("geometry" not in data2):
480
+ is_dupe = True
481
+
482
+ # if one edge has geometry attribute but the other doesn't: not dupes
483
+ else:
484
+ pass
485
+
486
+ return is_dupe
487
+
488
+
489
+ def _is_same_geometry(ls1: LineString, ls2: LineString) -> bool:
490
+ """
491
+ Determine if two LineString geometries are the same (in either direction).
492
+
493
+ Check both the normal and reversed orders of their constituent points.
494
+
495
+ Parameters
496
+ ----------
497
+ ls1
498
+ The first LineString geometry.
499
+ ls2
500
+ The second LineString geometry.
501
+
502
+ Returns
503
+ -------
504
+ is_same
505
+ True if geometries are the same in either direction, otherwise False.
506
+ """
507
+ # extract coordinates from each LineString geometry
508
+ geom1 = [tuple(coords) for coords in ls1.xy]
509
+ geom2 = [tuple(coords) for coords in ls2.xy]
510
+
511
+ # reverse the first LineString's coordinates' direction
512
+ geom1_r = [tuple(reversed(coords)) for coords in ls1.xy]
513
+
514
+ # if second geometry matches first in either direction, return True
515
+ return geom2 in (geom1, geom1_r)
516
+
517
+
518
+ def _update_edge_keys(G: nx.MultiDiGraph) -> nx.MultiDiGraph:
519
+ """
520
+ Increment key of one edge of parallel edges that differ in geometry.
521
+
522
+ For example, two streets from `u` to `v` that bow away from each other as
523
+ separate streets, rather than opposite direction edges of a single street.
524
+ Increment one of these edge's keys so that they do not match across
525
+ `(u, v, k)` or `(v, u, k)` so we can add both to an undirected MultiGraph.
526
+
527
+ Parameters
528
+ ----------
529
+ G
530
+ Input graph.
531
+
532
+ Returns
533
+ -------
534
+ G
535
+ Graph with incremented keys where needed.
536
+ """
537
+ # identify all the edges that are duplicates based on a sorted combination
538
+ # of their origin, destination, and key. that is, edge uv will match edge vu
539
+ # as a duplicate, but only if they have the same key
540
+ edges = graph_to_gdfs(G, nodes=False, fill_edge_geometry=False)
541
+ edges["uvk"] = ["_".join([*sorted([str(u), str(v)]), str(k)]) for u, v, k in edges.index]
542
+ mask = edges["uvk"].duplicated(keep=False)
543
+ dupes = edges[mask].dropna(subset=["geometry"])
544
+
545
+ different_streets = []
546
+ groups = dupes[["geometry", "uvk"]].groupby("uvk")
547
+
548
+ # for each group of duplicate edges
549
+ for _, group in groups:
550
+ # for each pair of edges within this group
551
+ for geom1, geom2 in itertools.combinations(group["geometry"], 2):
552
+ # if they don't have the same geometry, flag them as different
553
+ # streets: flag edge uvk, but not edge vuk, otherwise we would
554
+ # increment both their keys and they'll still duplicate each other
555
+ if not _is_same_geometry(geom1, geom2):
556
+ different_streets.append(group.index[0])
557
+
558
+ # for each unique different street, increment its key to make it unique
559
+ for u, v, k in set(different_streets):
560
+ new_key = max(list(G[u][v]) + list(G[v][u])) + 1
561
+ G.add_edge(u, v, key=new_key, **G.get_edge_data(u, v, k))
562
+ G.remove_edge(u, v, key=k)
563
+
564
+ return G
osmnx/source/osmnx/distance.py ADDED
@@ -0,0 +1,545 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Calculate distances and find nearest graph node/edge(s) to point(s)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging as lg
6
+ from collections.abc import Iterable
7
+ from typing import TYPE_CHECKING
8
+ from typing import Literal
9
+ from typing import overload
10
+
11
+ import networkx as nx
12
+ import numpy as np
13
+ import numpy.typing as npt
14
+ from shapely import Point
15
+ from shapely.strtree import STRtree
16
+
17
+ from . import convert
18
+ from . import projection
19
+ from . import utils
20
+
21
+ if TYPE_CHECKING:
22
+ from types import ModuleType
23
+
24
+ # scipy is optional dependency for projected nearest-neighbor search
25
+ scipy: ModuleType | None
26
+ try:
27
+ import scipy
28
+ except ImportError: # pragma: no cover
29
+ scipy = None
30
+
31
+ # scikit-learn is optional dependency for unprojected nearest-neighbor search
32
+ try:
33
+ from sklearn.neighbors import BallTree
34
+ except ImportError: # pragma: no cover
35
+ BallTree = None
36
+
37
+ EARTH_RADIUS_M = 6_371_009
38
+
39
+
40
+ # if coords are all floats, return float
41
+ @overload
42
+ def great_circle(lat1: float, lon1: float, lat2: float, lon2: float) -> float: ...
43
+
44
+
45
+ # if coords are all floats (and optional arg is provided), return float
46
+ @overload
47
+ def great_circle(
48
+ lat1: float,
49
+ lon1: float,
50
+ lat2: float,
51
+ lon2: float,
52
+ earth_radius: float,
53
+ ) -> float: ...
54
+
55
+
56
+ # if coords are all arrays, return array
57
+ @overload
58
+ def great_circle(
59
+ lat1: npt.NDArray[np.float64],
60
+ lon1: npt.NDArray[np.float64],
61
+ lat2: npt.NDArray[np.float64],
62
+ lon2: npt.NDArray[np.float64],
63
+ ) -> npt.NDArray[np.float64]: ...
64
+
65
+
66
+ # if coords are all arrays (and optional arg is provided), return array
67
+ @overload
68
+ def great_circle(
69
+ lat1: npt.NDArray[np.float64],
70
+ lon1: npt.NDArray[np.float64],
71
+ lat2: npt.NDArray[np.float64],
72
+ lon2: npt.NDArray[np.float64],
73
+ earth_radius: float,
74
+ ) -> npt.NDArray[np.float64]: ...
75
+
76
+
77
+ def great_circle(
78
+ lat1: float | npt.NDArray[np.float64],
79
+ lon1: float | npt.NDArray[np.float64],
80
+ lat2: float | npt.NDArray[np.float64],
81
+ lon2: float | npt.NDArray[np.float64],
82
+ earth_radius: float = EARTH_RADIUS_M,
83
+ ) -> float | npt.NDArray[np.float64]:
84
+ """
85
+ Calculate great-circle distances between pairs of points.
86
+
87
+ Vectorized function to calculate the great-circle distance between two
88
+ points' coordinates or between arrays of points' coordinates using the
89
+ haversine formula. Expects coordinates in decimal degrees.
90
+
91
+ Parameters
92
+ ----------
93
+ lat1
94
+ First point's latitude coordinate(s).
95
+ lon1
96
+ First point's longitude coordinate(s).
97
+ lat2
98
+ Second point's latitude coordinate(s).
99
+ lon2
100
+ Second point's longitude coordinate(s).
101
+ earth_radius
102
+ Earth's radius in units in which distance will be returned (default
103
+ represents meters).
104
+
105
+ Returns
106
+ -------
107
+ dist
108
+ Distance from each `(lat1, lon1)` point to each `(lat2, lon2)` point
109
+ in units of `earth_radius`.
110
+ """
111
+ y1 = np.deg2rad(lat1)
112
+ y2 = np.deg2rad(lat2)
113
+ delta_y = y2 - y1
114
+
115
+ x1 = np.deg2rad(lon1)
116
+ x2 = np.deg2rad(lon2)
117
+ delta_x = x2 - x1
118
+
119
+ h = np.sin(delta_y / 2) ** 2 + np.cos(y1) * np.cos(y2) * np.sin(delta_x / 2) ** 2
120
+ h = np.minimum(1, h) # protect against floating point errors
121
+ arc = 2 * np.arcsin(np.sqrt(h))
122
+
123
+ # return distance in units of earth_radius
124
+ dist: float | npt.NDArray[np.float64] = arc * earth_radius
125
+ return dist
126
+
127
+
128
+ # if coords are all floats, return float
129
+ @overload
130
+ def euclidean(y1: float, x1: float, y2: float, x2: float) -> float: ...
131
+
132
+
133
+ # if coords are all arrays, return array
134
+ @overload
135
+ def euclidean(
136
+ y1: npt.NDArray[np.float64],
137
+ x1: npt.NDArray[np.float64],
138
+ y2: npt.NDArray[np.float64],
139
+ x2: npt.NDArray[np.float64],
140
+ ) -> npt.NDArray[np.float64]: ...
141
+
142
+
143
+ def euclidean(
144
+ y1: float | npt.NDArray[np.float64],
145
+ x1: float | npt.NDArray[np.float64],
146
+ y2: float | npt.NDArray[np.float64],
147
+ x2: float | npt.NDArray[np.float64],
148
+ ) -> float | npt.NDArray[np.float64]:
149
+ """
150
+ Calculate Euclidean distances between pairs of points.
151
+
152
+ Vectorized function to calculate the Euclidean distance between two
153
+ points' coordinates or between arrays of points' coordinates. For accurate
154
+ results, use projected coordinates rather than decimal degrees.
155
+
156
+ Parameters
157
+ ----------
158
+ y1
159
+ First point's y coordinate(s).
160
+ x1
161
+ First point's x coordinate(s).
162
+ y2
163
+ Second point's y coordinate(s).
164
+ x2
165
+ Second point's x coordinate(s).
166
+
167
+ Returns
168
+ -------
169
+ dist
170
+ Distance from each `(x1, y1)` point to each `(x2, y2)` point in same
171
+ units as the points' coordinates.
172
+ """
173
+ # pythagorean theorem
174
+ dist: float | npt.NDArray[np.float64] = ((x1 - x2) ** 2 + (y1 - y2) ** 2) ** 0.5
175
+ return dist
176
+
177
+
178
+ def add_edge_lengths(
179
+ G: nx.MultiDiGraph,
180
+ *,
181
+ edges: Iterable[tuple[int, int, int]] | None = None,
182
+ ) -> nx.MultiDiGraph:
183
+ """
184
+ Calculate and add `length` attribute (in meters) to each edge.
185
+
186
+ Vectorized function to calculate great-circle distance between each edge's
187
+ incident nodes. Ensure graph is unprojected and unsimplified to calculate
188
+ accurate distances.
189
+
190
+ Note: this function is run by all the `graph.graph_from_x` functions
191
+ automatically to add `length` attributes to all edges. It calculates edge
192
+ lengths as the great-circle distance from node `u` to node `v`. When
193
+ OSMnx automatically runs this function upon graph creation, it does it
194
+ before simplifying the graph: thus it calculates the straight-line lengths
195
+ of edge segments that are themselves all straight. Only after
196
+ simplification do edges take on (potentially) curvilinear geometry. If you
197
+ wish to calculate edge lengths later, note that you will be calculating
198
+ straight-line distances which necessarily ignore the curvilinear geometry.
199
+ Thus you only want to run this function on a graph with all straight edges
200
+ (such as is the case with an unsimplified graph).
201
+
202
+ Parameters
203
+ ----------
204
+ G
205
+ Unprojected and unsimplified input graph.
206
+ edges
207
+ The subset of edges to add `length` attributes to, as `(u, v, k)`
208
+ tuples. If None, add lengths to all edges.
209
+
210
+ Returns
211
+ -------
212
+ G
213
+ Graph with `length` attributes on the edges.
214
+ """
215
+ uvk = G.edges if edges is None else edges
216
+
217
+ # extract edge IDs and corresponding coordinates from their nodes
218
+ x = G.nodes(data="x")
219
+ y = G.nodes(data="y")
220
+ msg = "Some edges missing nodes, possibly due to input data clipping issue."
221
+ try:
222
+ # two-dimensional array of coordinates: y0, x0, y1, x1
223
+ c = np.array([(y[u], x[u], y[v], x[v]) for u, v, k in uvk])
224
+ except KeyError as e: # pragma: no cover
225
+ raise ValueError(msg) from e
226
+ else:
227
+ # ensure all coordinates can be converted to float and are non-null
228
+ if np.isnan(c.astype(float)).any():
229
+ raise ValueError(msg)
230
+
231
+ # calculate great circle distances, round, and fill nulls with zeros
232
+ dists = great_circle(c[:, 0], c[:, 1], c[:, 2], c[:, 3])
233
+ dists[np.isnan(dists)] = 0
234
+ nx.set_edge_attributes(G, values=dict(zip(uvk, dists, strict=True)), name="length")
235
+
236
+ msg = "Added length attributes to graph edges"
237
+ utils.log(msg, level=lg.INFO)
238
+ return G
239
+
240
+
241
+ # if X and Y are floats and return_dist is not provided (defaults False)
242
+ @overload
243
+ def nearest_nodes(G: nx.MultiDiGraph, X: float, Y: float) -> int: ...
244
+
245
+
246
+ # if X and Y are floats and return_dist is provided/False
247
+ @overload
248
+ def nearest_nodes(
249
+ G: nx.MultiDiGraph,
250
+ X: float,
251
+ Y: float,
252
+ *,
253
+ return_dist: Literal[False],
254
+ ) -> int: ...
255
+
256
+
257
+ # if X and Y are floats and return_dist is provided/True
258
+ @overload
259
+ def nearest_nodes(
260
+ G: nx.MultiDiGraph,
261
+ X: float,
262
+ Y: float,
263
+ *,
264
+ return_dist: Literal[True],
265
+ ) -> tuple[npt.NDArray[np.int64], npt.NDArray[np.float64]]: ...
266
+
267
+
268
+ # if X and Y are iterable and return_dist is not provided (defaults False)
269
+ @overload
270
+ def nearest_nodes(
271
+ G: nx.MultiDiGraph,
272
+ X: Iterable[float],
273
+ Y: Iterable[float],
274
+ ) -> npt.NDArray[np.int64]: ...
275
+
276
+
277
+ # if X and Y are iterable and return_dist is provided/False
278
+ @overload
279
+ def nearest_nodes(
280
+ G: nx.MultiDiGraph,
281
+ X: Iterable[float],
282
+ Y: Iterable[float],
283
+ *,
284
+ return_dist: Literal[False],
285
+ ) -> npt.NDArray[np.int64]: ...
286
+
287
+
288
+ # if X and Y are iterable and return_dist is provided/True
289
+ @overload
290
+ def nearest_nodes(
291
+ G: nx.MultiDiGraph,
292
+ X: Iterable[float],
293
+ Y: Iterable[float],
294
+ *,
295
+ return_dist: Literal[True],
296
+ ) -> tuple[npt.NDArray[np.int64], npt.NDArray[np.float64]]: ...
297
+
298
+
299
+ def nearest_nodes(
300
+ G: nx.MultiDiGraph,
301
+ X: float | Iterable[float],
302
+ Y: float | Iterable[float],
303
+ *,
304
+ return_dist: bool = False,
305
+ ) -> (
306
+ int
307
+ | npt.NDArray[np.int64]
308
+ | tuple[int, float]
309
+ | tuple[npt.NDArray[np.int64], npt.NDArray[np.float64]]
310
+ ):
311
+ """
312
+ Find the nearest node to a point or to each of several points.
313
+
314
+ If `X` and `Y` are single coordinate values, this function will return the
315
+ nearest node to that point. If `X` and `Y` are iterables of coordinate
316
+ values, it will return the nearest node to each point.
317
+
318
+ This function is vectorized: if you have many points to search for, pass
319
+ them in one call as numpy arrays (avoid using loops) to maximize runtime
320
+ speed. If the graph is projected, it uses a k-d tree for Euclidean nearest
321
+ neighbor search, which requires that scipy is installed as an optional
322
+ dependency. If the graph is unprojected, it uses a ball tree for haversine
323
+ nearest neighbor search, which requires that scikit-learn is installed as
324
+ an optional dependency.
325
+
326
+ Parameters
327
+ ----------
328
+ G
329
+ Graph in which to find nearest nodes.
330
+ X
331
+ The points' x (longitude) coordinates, in same CRS/units as graph and
332
+ containing no nulls.
333
+ Y
334
+ The points' y (latitude) coordinates, in same CRS/units as graph and
335
+ containing no nulls.
336
+ return_dist
337
+ If True, optionally also return the distance(s) between point(s) and
338
+ nearest node(s).
339
+
340
+ Returns
341
+ -------
342
+ nn or (nn, dist)
343
+ Nearest node ID(s) or optionally a tuple of ID(s) and distance(s)
344
+ between each point and its nearest node.
345
+ """
346
+ # make coordinates arrays whether user passed iterable values or not
347
+ if not (isinstance(X, Iterable) and isinstance(Y, Iterable)):
348
+ is_scalar = True
349
+ X_arr = np.array([X])
350
+ Y_arr = np.array([Y])
351
+ else:
352
+ is_scalar = False
353
+ X_arr = np.array(X)
354
+ Y_arr = np.array(Y)
355
+
356
+ if np.isnan(X_arr).any() or np.isnan(Y_arr).any(): # pragma: no cover
357
+ msg = "`X` and `Y` cannot contain nulls."
358
+ raise ValueError(msg)
359
+
360
+ nodes = convert.graph_to_gdfs(G, edges=False, node_geometry=False)[["x", "y"]]
361
+ nn_array: npt.NDArray[np.int64]
362
+ dist_array: npt.NDArray[np.float64]
363
+
364
+ if projection.is_projected(G.graph["crs"]):
365
+ # if projected, use k-d tree for euclidean nearest-neighbor search
366
+ if scipy is None: # pragma: no cover
367
+ msg = "scipy must be installed as an optional dependency to search a projected graph."
368
+ raise ImportError(msg)
369
+ dist_array, pos = scipy.spatial.cKDTree(nodes).query(np.array([X_arr, Y_arr]).T, k=1)
370
+ nn_array = nodes.index[pos].to_numpy()
371
+
372
+ else:
373
+ # if unprojected, use ball tree for haversine nearest-neighbor search
374
+ if BallTree is None: # pragma: no cover
375
+ msg = "scikit-learn must be installed as an optional dependency to search an unprojected graph."
376
+ raise ImportError(msg)
377
+ # haversine requires lat, lon coords in radians
378
+ nodes_rad = np.deg2rad(nodes[["y", "x"]])
379
+ points_rad = np.deg2rad(np.array([Y_arr, X_arr]).T)
380
+ dist_array, pos = BallTree(nodes_rad, metric="haversine").query(points_rad, k=1)
381
+ dist_array = dist_array[:, 0] * EARTH_RADIUS_M # convert radians -> meters
382
+ nn_array = nodes.index[pos[:, 0]].to_numpy()
383
+
384
+ # convert results to correct types for return
385
+ if is_scalar:
386
+ nn = int(nn_array[0])
387
+ dist = float(dist_array[0])
388
+ if return_dist:
389
+ return nn, dist
390
+ # otherwise
391
+ return nn
392
+
393
+ # otherwise
394
+ if return_dist:
395
+ return nn_array, dist_array
396
+ # otherwise
397
+ return nn_array
398
+
399
+
400
+ # if X and Y are floats and return_dist is not provided (defaults False)
401
+ @overload
402
+ def nearest_edges(G: nx.MultiDiGraph, X: float, Y: float) -> tuple[int, int, int]: ...
403
+
404
+
405
+ # if X and Y are floats and return_dist is provided/False
406
+ @overload
407
+ def nearest_edges(
408
+ G: nx.MultiDiGraph,
409
+ X: float,
410
+ Y: float,
411
+ *,
412
+ return_dist: Literal[False],
413
+ ) -> tuple[int, int, int]: ...
414
+
415
+
416
+ # if X and Y are floats and return_dist is provided/True
417
+ @overload
418
+ def nearest_edges(
419
+ G: nx.MultiDiGraph,
420
+ X: float,
421
+ Y: float,
422
+ *,
423
+ return_dist: Literal[True],
424
+ ) -> tuple[tuple[int, int, int], float]: ...
425
+
426
+
427
+ # if X and Y are iterable and return_dist is not provided (defaults False)
428
+ @overload
429
+ def nearest_edges(
430
+ G: nx.MultiDiGraph,
431
+ X: Iterable[float],
432
+ Y: Iterable[float],
433
+ ) -> npt.NDArray[np.object_]: ...
434
+
435
+
436
+ # if X and Y are iterable and return_dist is provided/False
437
+ @overload
438
+ def nearest_edges(
439
+ G: nx.MultiDiGraph,
440
+ X: Iterable[float],
441
+ Y: Iterable[float],
442
+ *,
443
+ return_dist: Literal[False],
444
+ ) -> npt.NDArray[np.object_]: ...
445
+
446
+
447
+ # if X and Y are iterable and return_dist is provided/True
448
+ @overload
449
+ def nearest_edges(
450
+ G: nx.MultiDiGraph,
451
+ X: Iterable[float],
452
+ Y: Iterable[float],
453
+ *,
454
+ return_dist: Literal[True],
455
+ ) -> tuple[npt.NDArray[np.object_], npt.NDArray[np.float64]]: ...
456
+
457
+
458
+ def nearest_edges(
459
+ G: nx.MultiDiGraph,
460
+ X: float | Iterable[float],
461
+ Y: float | Iterable[float],
462
+ *,
463
+ return_dist: bool = False,
464
+ ) -> (
465
+ tuple[int, int, int]
466
+ | npt.NDArray[np.object_]
467
+ | tuple[tuple[int, int, int], float]
468
+ | tuple[npt.NDArray[np.object_], npt.NDArray[np.float64]]
469
+ ):
470
+ """
471
+ Find the nearest edge to a point or to each of several points.
472
+
473
+ If `X` and `Y` are single coordinate values, this function will return the
474
+ nearest edge to that point. If `X` and `Y` are iterables of coordinate
475
+ values, it will return the nearest edge to each point.
476
+
477
+ This function is vectorized: if you have many points to search for, pass
478
+ them in one call as numpy arrays (avoid using loops) to maximize runtime
479
+ speed. It uses an R-tree spatial index and minimizes the Euclidean
480
+ distance from each point to the possible matches. For accurate results,
481
+ use a projected graph and projected points.
482
+
483
+ Parameters
484
+ ----------
485
+ G
486
+ Graph in which to find nearest edges.
487
+ X
488
+ The points' x (longitude) coordinates, in same CRS/units as graph and
489
+ containing no nulls.
490
+ Y
491
+ The points' y (latitude) coordinates, in same CRS/units as graph and
492
+ containing no nulls.
493
+ return_dist
494
+ If True, optionally also return the distance(s) between point(s) and
495
+ nearest edge(s), in same units as graph and points.
496
+
497
+ Returns
498
+ -------
499
+ ne or (ne, dist)
500
+ Nearest edge ID(s) as `(u, v, k)` tuples, or optionally a tuple of
501
+ ID(s) and distance(s) between each point and its nearest edge.
502
+ """
503
+ # make coordinates arrays whether user passed iterable values or not
504
+ if not (isinstance(X, Iterable) and isinstance(Y, Iterable)):
505
+ is_scalar = True
506
+ X_arr = np.array([X])
507
+ Y_arr = np.array([Y])
508
+ else:
509
+ is_scalar = False
510
+ X_arr = np.array(X)
511
+ Y_arr = np.array(Y)
512
+
513
+ if np.isnan(X_arr).any() or np.isnan(Y_arr).any(): # pragma: no cover
514
+ msg = "`X` and `Y` cannot contain nulls."
515
+ raise ValueError(msg)
516
+ geoms = convert.graph_to_gdfs(G, nodes=False)["geometry"]
517
+ ne_array: npt.NDArray[np.object_] # array of tuple[int, int, int]
518
+ dist_array: npt.NDArray[np.float64]
519
+
520
+ # build an r-tree spatial index by position for subsequent iloc
521
+ rtree = STRtree(geoms)
522
+
523
+ # use the r-tree to find each point's nearest neighbor and distance
524
+ points = [Point(xy) for xy in zip(X_arr, Y_arr, strict=True)]
525
+ pos, dist_array = rtree.query_nearest(points, all_matches=False, return_distance=True)
526
+
527
+ # if user passed X/Y lists, the 2nd subarray contains geom indices
528
+ if len(pos.shape) > 1:
529
+ pos = pos[1]
530
+ ne_array = geoms.iloc[pos].index.to_numpy()
531
+
532
+ # convert results to correct types for return
533
+ if is_scalar:
534
+ ne: tuple[int, int, int] = ne_array[0]
535
+ dist = float(dist_array[0])
536
+ if return_dist:
537
+ return ne, dist
538
+ # otherwise
539
+ return ne
540
+
541
+ # otherwise
542
+ if return_dist:
543
+ return ne_array, dist_array
544
+ # otherwise
545
+ return ne_array
osmnx/source/osmnx/elevation.py ADDED
@@ -0,0 +1,331 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Add node elevations from raster files or web APIs, and calculate edge grades."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging as lg
6
+ import multiprocessing as mp
7
+ import time
8
+ from pathlib import Path
9
+ from typing import TYPE_CHECKING
10
+ from typing import Any
11
+
12
+ import networkx as nx
13
+ import numpy as np
14
+ import pandas as pd
15
+ import requests
16
+
17
+ from . import _http
18
+ from . import convert
19
+ from . import settings
20
+ from . import utils
21
+ from ._errors import InsufficientResponseError
22
+
23
+ if TYPE_CHECKING:
24
+ from collections.abc import Iterable
25
+
26
+ # rasterio and rio-vrt are optional dependencies for raster querying
27
+ try:
28
+ import rasterio
29
+ except ImportError: # pragma: no cover
30
+ rasterio = None
31
+ try:
32
+ from rio_vrt import build_vrt
33
+ except ImportError: # pragma: no cover
34
+ build_vrt = None
35
+
36
+
37
+ def add_edge_grades(G: nx.MultiDiGraph, *, add_absolute: bool = True) -> nx.MultiDiGraph:
38
+ """
39
+ Calculate and add `grade` attributes to all graph edges.
40
+
41
+ Vectorized function to calculate the directed grade (i.e., rise over run)
42
+ for each edge in the graph and add it to the edge as an attribute. Nodes
43
+ must already have `elevation` and `length` attributes before using this
44
+ function.
45
+
46
+ See also the `add_node_elevations_raster` and `add_node_elevations_google`
47
+ functions.
48
+
49
+ Parameters
50
+ ----------
51
+ G
52
+ Graph with `elevation` node attributes.
53
+ add_absolute
54
+ If True, also add absolute value of grade as `grade_abs` attribute.
55
+
56
+ Returns
57
+ -------
58
+ G
59
+ Graph with `grade` (and optionally `grade_abs`) attributes on the
60
+ edges.
61
+ """
62
+ elev_lookup = G.nodes(data="elevation")
63
+ u, v, k, lengths = zip(*G.edges(keys=True, data="length"), strict=True)
64
+ uvk = tuple(zip(u, v, k, strict=True))
65
+
66
+ # calculate edges' elevation changes from u to v then divide by lengths
67
+ elevs = np.array([(elev_lookup[u], elev_lookup[v]) for u, v, k in uvk])
68
+ grades = (elevs[:, 1] - elevs[:, 0]) / np.array(lengths)
69
+ nx.set_edge_attributes(G, dict(zip(uvk, grades, strict=True)), name="grade")
70
+
71
+ # optionally add grade absolute value to the edge attributes
72
+ if add_absolute:
73
+ nx.set_edge_attributes(G, dict(zip(uvk, np.abs(grades), strict=True)), name="grade_abs")
74
+
75
+ msg = "Added grade attributes to all edges"
76
+ utils.log(msg, level=lg.INFO)
77
+ return G
78
+
79
+
80
+ def _query_raster(
81
+ nodes: pd.DataFrame,
82
+ filepath: str | Path,
83
+ band: int,
84
+ ) -> Iterable[tuple[int, Any]]:
85
+ """
86
+ Query a raster file for values at coordinates in DataFrame x/y columns.
87
+
88
+ Parameters
89
+ ----------
90
+ nodes
91
+ DataFrame indexed by node ID and with two columns representing x and y
92
+ coordinates.
93
+ filepath
94
+ Path to the raster file or VRT to query.
95
+ band
96
+ Which raster band to query.
97
+
98
+ Returns
99
+ -------
100
+ nodes_values
101
+ Zip of node IDs and corresponding raster values.
102
+ """
103
+ # must open raster file here: cannot pickle it to pass in multiprocessing
104
+ with rasterio.open(filepath) as raster:
105
+ values = np.array(tuple(raster.sample(nodes.to_numpy(), band)), dtype=float).squeeze()
106
+ values[values == raster.nodata] = np.nan
107
+ return zip(nodes.index, values, strict=True)
108
+
109
+
110
+ def _build_vrt_file(raster_paths: Iterable[str | Path]) -> Path:
111
+ """
112
+ Build a virtual raster file compositing multiple individual raster files.
113
+
114
+ See also https://gdal.org/en/stable/drivers/raster/vrt.html
115
+
116
+ Parameters
117
+ ----------
118
+ raster_paths
119
+ The paths to the raster files.
120
+
121
+ Returns
122
+ -------
123
+ vrt_path
124
+ The path to the VRT file.
125
+ """
126
+ if build_vrt is None: # pragma: no cover
127
+ msg = "rio-vrt must be installed as an optional dependency to build VRTs."
128
+ raise ImportError(msg)
129
+
130
+ # determine VRT cache filepath, from stringified sorted raster filepaths
131
+ raster_paths = sorted(raster_paths)
132
+ vrt_path = _http._resolve_cache_filepath(str(raster_paths), "vrt")
133
+
134
+ # build the VRT file if it doesn't already exist in the cache
135
+ if not vrt_path.is_file():
136
+ msg = f"Building VRT for {len(raster_paths):,} rasters at {str(vrt_path)!r}..."
137
+ utils.log(msg, level=lg.INFO)
138
+ vrt_path.parent.mkdir(parents=True, exist_ok=True)
139
+ build_vrt(vrt_path, raster_paths)
140
+
141
+ return vrt_path
142
+
143
+
144
+ def add_node_elevations_raster(
145
+ G: nx.MultiDiGraph,
146
+ filepath: str | Path | Iterable[str | Path],
147
+ *,
148
+ band: int = 1,
149
+ cpus: int | None = None,
150
+ ) -> nx.MultiDiGraph:
151
+ """
152
+ Add `elevation` attributes to all nodes from local raster file(s).
153
+
154
+ If `filepath` is an iterable of paths, this will generate a virtual raster
155
+ composed of the files at those paths as an intermediate step.
156
+
157
+ See also the `add_edge_grades` function.
158
+
159
+ Parameters
160
+ ----------
161
+ G
162
+ Graph in same CRS as raster.
163
+ filepath
164
+ The path(s) to the raster file(s) to query.
165
+ band
166
+ Which raster band to query.
167
+ cpus
168
+ How many CPU cores to use if multiprocessing. If None, use all
169
+ available. If you are multiprocessing, make sure you protect your
170
+ entry point: see the Python docs for details.
171
+
172
+ Returns
173
+ -------
174
+ G
175
+ Graph with `elevation` attributes on the nodes.
176
+ """
177
+ if rasterio is None: # pragma: no cover
178
+ msg = "rasterio must be installed as an optional dependency to query rasters."
179
+ raise ImportError(msg)
180
+
181
+ # if multiple filepaths are passed in, compose them as a virtual raster
182
+ if not isinstance(filepath, (str, Path)):
183
+ filepath = _build_vrt_file(filepath)
184
+
185
+ if cpus is None:
186
+ cpus = mp.cpu_count()
187
+ cpus = min(cpus, mp.cpu_count())
188
+ msg = f"Attaching elevations with {cpus} CPUs..."
189
+ utils.log(msg, level=lg.INFO)
190
+
191
+ nodes = convert.graph_to_gdfs(G, edges=False, node_geometry=False)[["x", "y"]]
192
+ if cpus == 1:
193
+ elevs = dict(_query_raster(nodes, filepath, band))
194
+ else:
195
+ # divide nodes into equal-sized chunks for multiprocessing
196
+ size = int(np.ceil(len(nodes) / cpus))
197
+ args = ((nodes.iloc[i : i + size], filepath, band) for i in range(0, len(nodes), size))
198
+ with mp.get_context().Pool(cpus) as pool:
199
+ results = pool.starmap_async(_query_raster, args).get()
200
+ elevs = {k: v for kv in results for k, v in kv}
201
+
202
+ nx.set_node_attributes(G, elevs, name="elevation")
203
+ msg = "Added elevation data from raster to all nodes"
204
+ utils.log(msg, level=lg.INFO)
205
+ return G
206
+
207
+
208
+ def add_node_elevations_google(
209
+ G: nx.MultiDiGraph,
210
+ *,
211
+ api_key: str | None = None,
212
+ batch_size: int = 512,
213
+ pause: float = 0,
214
+ ) -> nx.MultiDiGraph:
215
+ """
216
+ Add `elevation` (meters) attributes to all nodes using a web API.
217
+
218
+ By default this uses the Google Maps Elevation API, but you could instead
219
+ use any equivalent API with the same interface and response format (such
220
+ as the Open Topo Data API or the Open-Elevation API) via the `settings`
221
+ module's `elevation_url_template`. Adjust the `batch_size` and `pause`
222
+ arguments as needed for the provider. The Google Maps Elevation API
223
+ requires an API key but other providers may not. You can find more
224
+ information about the Google Maps Elevation API interface and format at:
225
+ https://developers.google.com/maps/documentation/elevation
226
+
227
+ For a free local alternative see the `add_node_elevations_raster`
228
+ function. See also the `add_edge_grades` function.
229
+
230
+ Parameters
231
+ ----------
232
+ G
233
+ Graph to add elevation data to.
234
+ api_key
235
+ A valid API key. Can be None if the API does not require a key.
236
+ batch_size
237
+ Max number of coordinate pairs to submit in each request (depends on
238
+ provider's limits). Google's limit is 512.
239
+ pause
240
+ How long to pause in seconds between API calls, which can be increased
241
+ if you get rate limited.
242
+
243
+ Returns
244
+ -------
245
+ G
246
+ Graph with `elevation` attributes on the nodes.
247
+ """
248
+ # make a pandas series of all the nodes' coordinates as "lat,lon" and
249
+ # round coordinates to 6 decimal places (approx 5 to 10 cm resolution)
250
+ node_points = pd.Series({n: f"{d['y']:.6f},{d['x']:.6f}" for n, d in G.nodes(data=True)})
251
+ n_calls = int(np.ceil(len(node_points) / batch_size))
252
+ hostname = _http._hostname_from_url(settings.elevation_url_template)
253
+
254
+ msg = f"Requesting node elevations from {hostname!r} in {n_calls} request(s)"
255
+ utils.log(msg, level=lg.INFO)
256
+
257
+ # break the series of coordinates into chunks of batch_size
258
+ # API format is locations=lat,lon|lat,lon|lat,lon|lat,lon...
259
+ results = []
260
+ for i in range(0, len(node_points), batch_size):
261
+ chunk = node_points.iloc[i : i + batch_size]
262
+ locations = "|".join(chunk)
263
+ url = settings.elevation_url_template.format(locations=locations, key=api_key)
264
+
265
+ # download and append these elevation results to list of all results
266
+ response_json = _elevation_request(url, pause)
267
+ if "results" in response_json and len(response_json["results"]) > 0:
268
+ results.extend(response_json["results"])
269
+ else:
270
+ raise InsufficientResponseError(str(response_json))
271
+
272
+ # sanity check that all our vectors have the same number of elements
273
+ msg = f"Graph has {len(G):,} nodes and we received {len(results):,} results from {hostname!r}"
274
+ utils.log(msg, level=lg.INFO)
275
+ if not (len(results) == len(G) == len(node_points)): # pragma: no cover
276
+ err_msg = f"{msg}\n{response_json}"
277
+ raise InsufficientResponseError(err_msg)
278
+
279
+ # add elevation as an attribute to the nodes
280
+ df_elev = pd.DataFrame(node_points, columns=["node_points"])
281
+ df_elev["elevation"] = [result["elevation"] for result in results]
282
+ nx.set_node_attributes(G, name="elevation", values=df_elev["elevation"].to_dict())
283
+ msg = f"Added elevation data from {hostname!r} to all nodes."
284
+ utils.log(msg, level=lg.INFO)
285
+
286
+ return G
287
+
288
+
289
+ def _elevation_request(url: str, pause: float) -> dict[str, Any]:
290
+ """
291
+ Send a HTTP GET request to a Google Maps-style elevation API.
292
+
293
+ Parameters
294
+ ----------
295
+ url
296
+ URL of API endpoint, populated with request data.
297
+ pause
298
+ How long to pause in seconds before request.
299
+
300
+ Returns
301
+ -------
302
+ response_json
303
+ The elevation API's response.
304
+ """
305
+ # check if request already exists in cache
306
+ cached_response_json = _http._retrieve_from_cache(url)
307
+ if isinstance(cached_response_json, dict):
308
+ return cached_response_json
309
+
310
+ # pause then request this URL
311
+ hostname = _http._hostname_from_url(url)
312
+ msg = f"Pausing {pause} second(s) before making HTTP GET request to {hostname!r}"
313
+ utils.log(msg, level=lg.INFO)
314
+ time.sleep(pause)
315
+
316
+ # transmit the HTTP GET request
317
+ msg = f"Get {url} with timeout={settings.requests_timeout}"
318
+ utils.log(msg, level=lg.INFO)
319
+ response = requests.get(
320
+ url,
321
+ timeout=settings.requests_timeout,
322
+ headers=_http._get_http_headers(),
323
+ **settings.requests_kwargs,
324
+ )
325
+
326
+ response_json = _http._parse_response(response)
327
+ if not isinstance(response_json, dict): # pragma: no cover
328
+ msg = "Elevation API did not return a dict of results."
329
+ raise InsufficientResponseError(msg)
330
+ _http._save_to_cache(url, response_json, response.ok)
331
+ return response_json
osmnx/source/osmnx/features.py ADDED
@@ -0,0 +1,734 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Download and create GeoDataFrames from OpenStreetMap geospatial features.
3
+
4
+ Retrieve points of interest, building footprints, transit lines/stops, or any
5
+ other map features from OSM, including their geometries and attribute data,
6
+ then construct a GeoDataFrame of them. You can use this module to query for
7
+ nodes, ways, and relations (the latter of type "multipolygon" or "boundary"
8
+ only) by passing a dictionary of desired OSM tags.
9
+
10
+ For more details, see https://wiki.openstreetmap.org/wiki/Map_features and
11
+ https://wiki.openstreetmap.org/wiki/Elements
12
+
13
+ Refer to the Getting Started guide for usage limitations.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging as lg
19
+ from pathlib import Path
20
+ from typing import TYPE_CHECKING
21
+ from typing import Any
22
+
23
+ import geopandas as gpd
24
+ import pandas as pd
25
+ from shapely import LineString
26
+ from shapely import MultiLineString
27
+ from shapely import MultiPolygon
28
+ from shapely import Point
29
+ from shapely import Polygon
30
+ from shapely import prepare
31
+ from shapely.errors import GEOSException
32
+ from shapely.ops import linemerge
33
+ from shapely.ops import polygonize
34
+ from shapely.ops import unary_union
35
+
36
+ from . import _osm_xml
37
+ from . import _overpass
38
+ from . import geocoder
39
+ from . import settings
40
+ from . import utils
41
+ from . import utils_geo
42
+ from ._errors import CacheOnlyInterruptError
43
+ from ._errors import InsufficientResponseError
44
+
45
+ if TYPE_CHECKING:
46
+ from collections.abc import Iterable
47
+
48
+ # define what types of OSM relations we currently handle
49
+ _RELATION_TYPES = {"boundary", "multipolygon"}
50
+
51
+ # OSM tags to determine if closed ways should be polygons, based on JSON from
52
+ # https://wiki.openstreetmap.org/wiki/Overpass_turbo/Polygon_Features
53
+ _POLYGON_FEATURES: dict[str, dict[str, str | set[str]]] = {
54
+ "aeroway": {"polygon": "blocklist", "values": {"taxiway"}},
55
+ "amenity": {"polygon": "all"},
56
+ "area": {"polygon": "all"},
57
+ "area:highway": {"polygon": "all"},
58
+ "barrier": {
59
+ "polygon": "passlist",
60
+ "values": {"city_wall", "ditch", "hedge", "retaining_wall", "spikes"},
61
+ },
62
+ "boundary": {"polygon": "all"},
63
+ "building": {"polygon": "all"},
64
+ "building:part": {"polygon": "all"},
65
+ "craft": {"polygon": "all"},
66
+ "golf": {"polygon": "all"},
67
+ "highway": {"polygon": "passlist", "values": {"elevator", "escape", "rest_area", "services"}},
68
+ "historic": {"polygon": "all"},
69
+ "indoor": {"polygon": "all"},
70
+ "landuse": {"polygon": "all"},
71
+ "leisure": {"polygon": "all"},
72
+ "man_made": {"polygon": "blocklist", "values": {"cutline", "embankment", "pipeline"}},
73
+ "military": {"polygon": "all"},
74
+ "natural": {
75
+ "polygon": "blocklist",
76
+ "values": {"arete", "cliff", "coastline", "ridge", "tree_row"},
77
+ },
78
+ "office": {"polygon": "all"},
79
+ "place": {"polygon": "all"},
80
+ "power": {"polygon": "passlist", "values": {"generator", "plant", "substation", "transformer"}},
81
+ "public_transport": {"polygon": "all"},
82
+ "railway": {
83
+ "polygon": "passlist",
84
+ "values": {"platform", "roundhouse", "station", "turntable"},
85
+ },
86
+ "ruins": {"polygon": "all"},
87
+ "shop": {"polygon": "all"},
88
+ "tourism": {"polygon": "all"},
89
+ "waterway": {"polygon": "passlist", "values": {"boatyard", "dam", "dock", "riverbank"}},
90
+ }
91
+
92
+
93
+ def features_from_bbox(
94
+ bbox: tuple[float, float, float, float],
95
+ tags: dict[str, bool | str | list[str]],
96
+ ) -> gpd.GeoDataFrame:
97
+ """
98
+ Download OSM features within a lat-lon bounding box.
99
+
100
+ You can use the `settings` module to retrieve a snapshot of historical OSM
101
+ data as of a certain date, or to configure the Overpass server timeout,
102
+ memory allocation, and other custom settings. This function searches for
103
+ features using tags. For more details, see:
104
+ https://wiki.openstreetmap.org/wiki/Map_features
105
+
106
+ Parameters
107
+ ----------
108
+ bbox
109
+ Bounding box as `(left, bottom, right, top)`. Coordinates should be in
110
+ unprojected latitude-longitude degrees (EPSG:4326).
111
+ tags
112
+ Tags for finding elements in the selected area. Results are the union,
113
+ not intersection of the tags and each result matches at least one tag.
114
+ The keys are OSM tags (e.g. `building`, `landuse`, `highway`, etc) and
115
+ the values can be either `True` to retrieve all elements matching the
116
+ tag, or a string to retrieve a single `tag:value` combination, or a
117
+ list of strings to retrieve multiple values for the tag. For example,
118
+ `tags = {'building': True}` would return all buildings in the area.
119
+ Or, `tags = {'amenity':True, 'landuse':['retail','commercial'],
120
+ 'highway':'bus_stop'}` would return all amenities, any landuse=retail,
121
+ any landuse=commercial, and any highway=bus_stop.
122
+
123
+ Returns
124
+ -------
125
+ gdf
126
+ The features, multi-indexed by element type and OSM ID.
127
+ """
128
+ # convert bbox to polygon then create GeoDataFrame of features within it
129
+ polygon = utils_geo.bbox_to_poly(bbox)
130
+ return features_from_polygon(polygon, tags)
131
+
132
+
133
+ def features_from_point(
134
+ center_point: tuple[float, float],
135
+ tags: dict[str, bool | str | list[str]],
136
+ dist: float,
137
+ ) -> gpd.GeoDataFrame:
138
+ """
139
+ Download OSM features within some distance of a lat-lon point.
140
+
141
+ You can use the `settings` module to retrieve a snapshot of historical OSM
142
+ data as of a certain date, or to configure the Overpass server timeout,
143
+ memory allocation, and other custom settings. This function searches for
144
+ features using tags. For more details, see:
145
+ https://wiki.openstreetmap.org/wiki/Map_features
146
+
147
+ Parameters
148
+ ----------
149
+ center_point
150
+ The `(lat, lon)` center point around which to retrieve the features.
151
+ Coordinates should be in unprojected latitude-longitude degrees
152
+ (EPSG:4326).
153
+ tags
154
+ Tags for finding elements in the selected area. Results are the union,
155
+ not intersection of the tags and each result matches at least one tag.
156
+ The keys are OSM tags (e.g. `building`, `landuse`, `highway`, etc) and
157
+ the values can be either `True` to retrieve all elements matching the
158
+ tag, or a string to retrieve a single `tag:value` combination, or a
159
+ list of strings to retrieve multiple values for the tag. For example,
160
+ `tags = {'building': True}` would return all buildings in the area.
161
+ Or, `tags = {'amenity':True, 'landuse':['retail','commercial'],
162
+ 'highway':'bus_stop'}` would return all amenities, any landuse=retail,
163
+ any landuse=commercial, and any highway=bus_stop.
164
+ dist
165
+ Distance in meters from `center_point` to create a bounding box to
166
+ query.
167
+
168
+ Returns
169
+ -------
170
+ gdf
171
+ The features, multi-indexed by element type and OSM ID.
172
+ """
173
+ # create bbox from point and dist, then create gdf of features within it
174
+ bbox = utils_geo.bbox_from_point(center_point, dist)
175
+ return features_from_bbox(bbox, tags)
176
+
177
+
178
+ def features_from_address(
179
+ address: str,
180
+ tags: dict[str, bool | str | list[str]],
181
+ dist: float,
182
+ ) -> gpd.GeoDataFrame:
183
+ """
184
+ Download OSM features within some distance of an address.
185
+
186
+ You can use the `settings` module to retrieve a snapshot of historical OSM
187
+ data as of a certain date, or to configure the Overpass server timeout,
188
+ memory allocation, and other custom settings. This function searches for
189
+ features using tags. For more details, see:
190
+ https://wiki.openstreetmap.org/wiki/Map_features
191
+
192
+ Parameters
193
+ ----------
194
+ address
195
+ The address to geocode and use as the center point around which to
196
+ retrieve the features.
197
+ tags
198
+ Tags for finding elements in the selected area. Results are the union,
199
+ not intersection of the tags and each result matches at least one tag.
200
+ The keys are OSM tags (e.g. `building`, `landuse`, `highway`, etc) and
201
+ the values can be either `True` to retrieve all elements matching the
202
+ tag, or a string to retrieve a single `tag:value` combination, or a
203
+ list of strings to retrieve multiple values for the tag. For example,
204
+ `tags = {'building': True}` would return all buildings in the area.
205
+ Or, `tags = {'amenity':True, 'landuse':['retail','commercial'],
206
+ 'highway':'bus_stop'}` would return all amenities, any landuse=retail,
207
+ any landuse=commercial, and any highway=bus_stop.
208
+ dist
209
+ Distance in meters from `address` to create a bounding box to query.
210
+
211
+ Returns
212
+ -------
213
+ gdf
214
+ The features, multi-indexed by element type and OSM ID.
215
+ """
216
+ # geocode the address to a point, then create gdf of features around it
217
+ center_point = geocoder.geocode(address)
218
+ return features_from_point(center_point, tags, dist)
219
+
220
+
221
+ def features_from_place(
222
+ query: str | dict[str, str] | list[str | dict[str, str]],
223
+ tags: dict[str, bool | str | list[str]],
224
+ *,
225
+ which_result: int | None | list[int | None] = None,
226
+ ) -> gpd.GeoDataFrame:
227
+ """
228
+ Download OSM features within the boundaries of some place(s).
229
+
230
+ The query must be geocodable and OSM must have polygon boundaries for the
231
+ geocode result. If OSM does not have a polygon for this place, you can
232
+ instead get features within it using the `features_from_address`
233
+ function, which geocodes the place name to a point and gets the features
234
+ within some distance of that point.
235
+
236
+ If OSM does have polygon boundaries for this place but you're not finding
237
+ it, try to vary the query string, pass in a structured query dict, or vary
238
+ the `which_result` argument to use a different geocode result. If you know
239
+ the OSM ID of the place, you can retrieve its boundary polygon using the
240
+ `geocode_to_gdf` function, then pass it to the `features_from_polygon`
241
+ function.
242
+
243
+ You can use the `settings` module to retrieve a snapshot of historical OSM
244
+ data as of a certain date, or to configure the Overpass server timeout,
245
+ memory allocation, and other custom settings. This function searches for
246
+ features using tags. For more details, see:
247
+ https://wiki.openstreetmap.org/wiki/Map_features
248
+
249
+ Parameters
250
+ ----------
251
+ query
252
+ The query or queries to geocode to retrieve place boundary polygon(s).
253
+ tags
254
+ Tags for finding elements in the selected area. Results are the union,
255
+ not intersection of the tags and each result matches at least one tag.
256
+ The keys are OSM tags (e.g. `building`, `landuse`, `highway`, etc) and
257
+ the values can be either `True` to retrieve all elements matching the
258
+ tag, or a string to retrieve a single `tag:value` combination, or a
259
+ list of strings to retrieve multiple values for the tag. For example,
260
+ `tags = {'building': True}` would return all buildings in the area.
261
+ Or, `tags = {'amenity':True, 'landuse':['retail','commercial'],
262
+ 'highway':'bus_stop'}` would return all amenities, any landuse=retail,
263
+ any landuse=commercial, and any highway=bus_stop.
264
+ which_result
265
+ Which search result to return. If None, auto-select the first
266
+ (Multi)Polygon or raise an error if OSM doesn't return one.
267
+
268
+ Returns
269
+ -------
270
+ gdf
271
+ The features, multi-indexed by element type and OSM ID.
272
+ """
273
+ # extract the geometry from the GeoDataFrame to use in query
274
+ polygon = geocoder.geocode_to_gdf(query, which_result=which_result).union_all()
275
+ msg = "Constructed place geometry polygon(s) to query Overpass"
276
+ utils.log(msg, level=lg.INFO)
277
+
278
+ # create GeoDataFrame using this polygon(s) geometry
279
+ return features_from_polygon(polygon, tags)
280
+
281
+
282
+ def features_from_polygon(
283
+ polygon: Polygon | MultiPolygon,
284
+ tags: dict[str, bool | str | list[str]],
285
+ ) -> gpd.GeoDataFrame:
286
+ """
287
+ Download OSM features within the boundaries of a (Multi)Polygon.
288
+
289
+ You can use the `settings` module to retrieve a snapshot of historical OSM
290
+ data as of a certain date, or to configure the Overpass server timeout,
291
+ memory allocation, and other custom settings. This function searches for
292
+ features using tags. For more details, see:
293
+ https://wiki.openstreetmap.org/wiki/Map_features
294
+
295
+ Parameters
296
+ ----------
297
+ polygon
298
+ The geometry within which to retrieve features. Coordinates should be
299
+ in unprojected latitude-longitude degrees (EPSG:4326).
300
+ tags
301
+ Tags for finding elements in the selected area. Results are the union,
302
+ not intersection of the tags and each result matches at least one tag.
303
+ The keys are OSM tags (e.g. `building`, `landuse`, `highway`, etc) and
304
+ the values can be either `True` to retrieve all elements matching the
305
+ tag, or a string to retrieve a single `tag:value` combination, or a
306
+ list of strings to retrieve multiple values for the tag. For example,
307
+ `tags = {'building': True}` would return all buildings in the area.
308
+ Or, `tags = {'amenity':True, 'landuse':['retail','commercial'],
309
+ 'highway':'bus_stop'}` would return all amenities, any landuse=retail,
310
+ any landuse=commercial, and any highway=bus_stop.
311
+
312
+ Returns
313
+ -------
314
+ gdf
315
+ The features, multi-indexed by element type and OSM ID.
316
+ """
317
+ # verify that the geometry is valid and is a Polygon/MultiPolygon
318
+ if not polygon.is_valid:
319
+ msg = "The geometry of `polygon` is invalid."
320
+ raise ValueError(msg)
321
+
322
+ if not isinstance(polygon, (Polygon, MultiPolygon)):
323
+ msg = (
324
+ "Boundaries must be a Polygon or MultiPolygon. If you requested "
325
+ "`features_from_place`, ensure your query geocodes to a Polygon "
326
+ "or MultiPolygon. See the documentation for details."
327
+ )
328
+ raise TypeError(msg)
329
+
330
+ # retrieve the data from Overpass then turn it into a GeoDataFrame
331
+ response_jsons = _overpass._download_overpass_features(polygon, tags)
332
+ return _create_gdf(response_jsons, polygon, tags)
333
+
334
+
335
+ def features_from_xml(
336
+ filepath: str | Path,
337
+ *,
338
+ polygon: Polygon | MultiPolygon | None = None,
339
+ tags: dict[str, bool | str | list[str]] | None = None,
340
+ encoding: str = "utf-8",
341
+ ) -> gpd.GeoDataFrame:
342
+ """
343
+ Create a GeoDataFrame of OSM features from data in an OSM XML file.
344
+
345
+ Because this function creates a GeoDataFrame of features from an OSM XML
346
+ file that has already been downloaded (i.e., no query is made to the
347
+ Overpass API), the `polygon` and `tags` arguments are optional. If they
348
+ are None, filtering will be skipped.
349
+
350
+ Parameters
351
+ ----------
352
+ filepath
353
+ Path to file containing OSM XML data.
354
+ polygon
355
+ Spatial boundaries to optionally filter the final GeoDataFrame.
356
+ tags
357
+ Query tags to optionally filter the final GeoDataFrame.
358
+ encoding
359
+ The OSM XML file's character encoding.
360
+
361
+ Returns
362
+ -------
363
+ gdf
364
+ The features, multi-indexed by element type and OSM ID.
365
+ """
366
+ # if tags or polygon is None, create an empty object to skip filtering
367
+ if tags is None:
368
+ tags = {}
369
+ if polygon is None:
370
+ polygon = Polygon()
371
+
372
+ # transmogrify OSM XML file to JSON then create GeoDataFrame from it
373
+ response_jsons = [_osm_xml._overpass_json_from_xml(Path(filepath), encoding)]
374
+ gdf = _create_gdf(response_jsons, polygon, tags)
375
+
376
+ # drop misc element attrs that might have been added from OSM XML file
377
+ to_drop = set(gdf.columns) & {"changeset", "timestamp", "uid", "user", "version"}
378
+ return gdf.drop(columns=list(to_drop))
379
+
380
+
381
+ def _create_gdf(
382
+ response_jsons: Iterable[dict[str, Any]],
383
+ polygon: Polygon | MultiPolygon,
384
+ tags: dict[str, bool | str | list[str]],
385
+ ) -> gpd.GeoDataFrame:
386
+ """
387
+ Convert Overpass API JSON responses to a GeoDataFrame of features.
388
+
389
+ Parameters
390
+ ----------
391
+ response_jsons
392
+ Iterable of Overpass API JSON responses.
393
+ polygon
394
+ Spatial boundaries to optionally filter the final GeoDataFrame.
395
+ tags
396
+ Query tags to optionally filter the final GeoDataFrame.
397
+
398
+ Returns
399
+ -------
400
+ gdf
401
+ GeoDataFrame of features with tags and geometry columns.
402
+ """
403
+ # consume response_jsons generator to download data from server
404
+ elements = []
405
+ response_count = 0
406
+ for response_json in response_jsons:
407
+ response_count += 1
408
+ if not settings.cache_only_mode:
409
+ elements.extend(response_json["elements"])
410
+
411
+ msg = f"Retrieved {len(elements):,} elements from API in {response_count} request(s)"
412
+ utils.log(msg, level=lg.INFO)
413
+ if settings.cache_only_mode:
414
+ msg = "Interrupted because `settings.cache_only_mode=True`."
415
+ raise CacheOnlyInterruptError(msg)
416
+
417
+ # convert the elements into a GeoDataFrame of features
418
+ gdf = (
419
+ gpd.GeoDataFrame(
420
+ data=_process_features(elements, set(tags.keys())),
421
+ geometry="geometry",
422
+ crs=settings.default_crs,
423
+ )
424
+ .set_index(["element", "id"])
425
+ .sort_index()
426
+ )
427
+ return _filter_features(gdf, polygon, tags)
428
+
429
+
430
+ def _process_features(
431
+ elements: list[dict[str, Any]],
432
+ query_tag_keys: set[str],
433
+ ) -> list[dict[str, Any]]:
434
+ """
435
+ Convert node/way/relation elements into features with geometries.
436
+
437
+ Parameters
438
+ ----------
439
+ elements
440
+ The node/way/relation elements retrieved from the server.
441
+ query_tag_keys
442
+ The keys of the tags used to query for matching features.
443
+
444
+ Returns
445
+ -------
446
+ features
447
+ The features with geometries.
448
+ """
449
+ nodes = [] # all nodes, including ones that just compose ways
450
+ feature_nodes = [] # nodes that possibly match our query tags
451
+ node_coords = {} # hold node lon,lat tuples to create way geoms
452
+ ways = [] # all ways, including ones that just compose relations
453
+ feature_ways = [] # ways that possibly match our query tags
454
+ way_geoms = {} # hold way geoms to create relation geoms
455
+ relations = [] # all relations
456
+
457
+ # sort elements by node, way, and relation. only retain relations that
458
+ # match the relation types we currently handle. remove any geometry tags
459
+ # (they shouldn't exist) or they'll overwrite our geom attributes later
460
+ for element in elements:
461
+ element.get("tags", {}).pop("geometry", None)
462
+ et = element["type"]
463
+ if et == "node":
464
+ nodes.append(element)
465
+ elif et == "way":
466
+ ways.append(element)
467
+ elif et == "relation" and element.get("tags", {}).get("type") in _RELATION_TYPES:
468
+ relations.append(element)
469
+
470
+ # extract all nodes' coords, then add to features any nodes with tags that
471
+ # match the passed query tags, or with any tags if no query tags passed
472
+ for node in nodes:
473
+ node_coords[node["id"]] = (node["lon"], node["lat"])
474
+ if (len(query_tag_keys) == 0 and len(node.get("tags", {}).keys()) > 0) or (
475
+ len(query_tag_keys & node.get("tags", {}).keys()) > 0
476
+ ):
477
+ node["element"] = node.pop("type")
478
+ node["geometry"] = Point(node.pop("lon"), node.pop("lat"))
479
+ node.update(node.pop("tags"))
480
+ feature_nodes.append(node)
481
+
482
+ # build all ways' geometries, then add to features any ways with tags that
483
+ # match the passed query tags, or with any tags if no query tags passed
484
+ for way in ways:
485
+ way["geometry"] = _build_way_geometry(
486
+ way["id"],
487
+ way.pop("nodes"),
488
+ way.get("tags", {}),
489
+ node_coords,
490
+ )
491
+ way_geoms[way["id"]] = way["geometry"]
492
+ if (len(query_tag_keys) == 0 and len(way.get("tags", {}).keys()) > 0) or (
493
+ len(query_tag_keys & way.get("tags", {}).keys()) > 0
494
+ ):
495
+ way["element"] = way.pop("type")
496
+ way.update(way.pop("tags"))
497
+ feature_ways.append(way)
498
+
499
+ # process relations and build their geometries
500
+ for relation in relations:
501
+ relation["element"] = "relation"
502
+ relation.update(relation.pop("tags"))
503
+ relation["geometry"] = _build_relation_geometry(relation.pop("members"), way_geoms)
504
+
505
+ features = [*feature_nodes, *feature_ways, *relations]
506
+ if len(features) == 0:
507
+ msg = "No matching features. Check query location, tags, and log."
508
+ raise InsufficientResponseError(msg)
509
+
510
+ return features
511
+
512
+
513
+ def _build_way_geometry(
514
+ way_id: int,
515
+ way_nodes: list[int],
516
+ way_tags: dict[str, Any],
517
+ node_coords: dict[int, tuple[float, float]],
518
+ ) -> LineString | Polygon:
519
+ """
520
+ Build a way's geometry from its constituent nodes' coordinates.
521
+
522
+ A way can be a LineString (open or closed way) or a Polygon (closed way)
523
+ but multi-geometries and polygons with holes are represented as relations.
524
+ See documentation: https://wiki.openstreetmap.org/wiki/Way#Types_of_ways
525
+
526
+ Parameters
527
+ ----------
528
+ way_id
529
+ The way's OSM ID.
530
+ way_nodes
531
+ The way's constituent nodes.
532
+ way_tags
533
+ The way's tags.
534
+ node_coords
535
+ Keyed by OSM node ID with values of `(lat, lon)` coordinate tuples.
536
+
537
+ Returns
538
+ -------
539
+ geometry
540
+ The way's geometry.
541
+ """
542
+ # a way is a LineString by default, but if it's a closed way and it's not
543
+ # tagged area=no, check if any of its tags denote it as a polygon instead
544
+ geom_type = LineString
545
+ if way_nodes[0] == way_nodes[-1] and way_tags.get("area") != "no":
546
+ for tag in way_tags.keys() & _POLYGON_FEATURES.keys():
547
+ rule = _POLYGON_FEATURES[tag]["polygon"]
548
+ values = _POLYGON_FEATURES[tag].get("values", set())
549
+ if (
550
+ rule == "all"
551
+ or (rule == "passlist" and way_tags[tag] in values)
552
+ or (rule == "blocklist" and way_tags[tag] not in values)
553
+ ):
554
+ geom_type = Polygon
555
+ break
556
+
557
+ # create the way geometry from its constituent nodes' coordinates
558
+ try:
559
+ return geom_type(node_coords[node] for node in way_nodes)
560
+ except (GEOSException, KeyError, ValueError) as e:
561
+ msg = f"Could not build geometry of way {way_id}: {e!r}"
562
+ utils.log(msg, level=lg.WARNING)
563
+ return geom_type()
564
+
565
+
566
+ def _build_relation_geometry(
567
+ members: list[dict[str, Any]],
568
+ way_geoms: dict[int, LineString | Polygon],
569
+ ) -> Polygon | MultiPolygon:
570
+ """
571
+ Build a relation's geometry from its constituent member ways' geometries.
572
+
573
+ OSM represents simple polygons as closed ways (see `_build_way_geometry`),
574
+ but it uses relations to represent multipolygons (with or without holes)
575
+ and polygons with holes. For the former, the relation contains multiple
576
+ members with role "outer". For the latter, the relation contains at least
577
+ one member with role "outer" representing the shell(s), and at least one
578
+ member with role "inner" representing the hole(s). For documentation, see
579
+ https://wiki.openstreetmap.org/wiki/Relation:multipolygon
580
+
581
+ Parameters
582
+ ----------
583
+ members
584
+ The members constituting the relation.
585
+ way_geoms
586
+ Keyed by OSM way ID with values of their geometries.
587
+
588
+ Returns
589
+ -------
590
+ geometry
591
+ The relation's geometry.
592
+ """
593
+ inner_linestrings = []
594
+ outer_linestrings = []
595
+ inner_polygons = []
596
+ outer_polygons = []
597
+
598
+ # sort member geometries by member role and geometry type
599
+ for member in members:
600
+ if member["type"] == "way":
601
+ geom = way_geoms.get(member["ref"])
602
+ if geom is None:
603
+ # a member's geometry may be missing when loaded from XML, if
604
+ # so, we cannot build this relation's complete geometry, so
605
+ # just return a null geometry to be removed at final filtering
606
+ msg = f"Cannot build relation geometry, missing member way {member['ref']}"
607
+ utils.log(msg, level=lg.WARNING)
608
+ return Polygon()
609
+ role = member["role"]
610
+ if role == "outer" and geom.geom_type == "LineString":
611
+ outer_linestrings.append(geom)
612
+ elif role == "outer" and geom.geom_type == "Polygon":
613
+ outer_polygons.append(geom)
614
+ elif role == "inner" and geom.geom_type == "LineString":
615
+ inner_linestrings.append(geom)
616
+ elif role == "inner" and geom.geom_type == "Polygon":
617
+ inner_polygons.append(geom)
618
+
619
+ # merge/polygonize outer linestring fragments then add to outer polygons
620
+ merged_outer_linestrings = linemerge(outer_linestrings)
621
+ if merged_outer_linestrings.geom_type == "LineString":
622
+ merged_outer_linestrings = MultiLineString([merged_outer_linestrings])
623
+ for merged_outer_linestring in merged_outer_linestrings.geoms:
624
+ outer_polygons += polygonize(merged_outer_linestring)
625
+
626
+ # merge/polygonize inner linestring fragments then add to inner polygons
627
+ merged_inner_linestrings = linemerge(inner_linestrings)
628
+ if merged_inner_linestrings.geom_type == "LineString":
629
+ merged_inner_linestrings = MultiLineString([merged_inner_linestrings])
630
+ for merged_inner_linestring in merged_inner_linestrings.geoms:
631
+ inner_polygons += polygonize(merged_inner_linestring)
632
+
633
+ # remove holes from polygons, if any, then retun
634
+ return _remove_polygon_holes(outer_polygons, inner_polygons)
635
+
636
+
637
+ def _remove_polygon_holes(
638
+ outer_polygons: list[Polygon],
639
+ inner_polygons: list[Polygon],
640
+ ) -> Polygon | MultiPolygon:
641
+ """
642
+ Subtract inner holes from outer polygons.
643
+
644
+ This allows possible island polygons within a larger polygon's holes.
645
+
646
+ Parameters
647
+ ----------
648
+ outer_polygons
649
+ Polygons, including possible islands within a larger polygon's holes.
650
+ inner_polygons
651
+ Inner holes to subtract from the outer polygons that contain them.
652
+
653
+ Returns
654
+ -------
655
+ geometry
656
+ The geometry minus inner holes.
657
+ """
658
+ if len(inner_polygons) == 0:
659
+ # if there are no holes to remove, geom is the union of outer polygons
660
+ geometry = unary_union(outer_polygons)
661
+ else:
662
+ # otherwise, remove from each outer poly all inner polys it contains
663
+ polygons_with_holes = []
664
+ for outer in outer_polygons:
665
+ prepare(outer)
666
+ holes = [inner for inner in inner_polygons if outer.contains(inner)]
667
+ polygons_with_holes.append(outer.difference(unary_union(holes)))
668
+ geometry = unary_union(polygons_with_holes)
669
+
670
+ # ensure returned geometry is a Polygon or MultiPolygon
671
+ if isinstance(geometry, (Polygon, MultiPolygon)):
672
+ return geometry
673
+ return Polygon()
674
+
675
+
676
+ def _filter_features(
677
+ gdf: gpd.GeoDataFrame,
678
+ polygon: Polygon | MultiPolygon,
679
+ tags: dict[str, bool | str | list[str]],
680
+ ) -> gpd.GeoDataFrame:
681
+ """
682
+ Filter features GeoDataFrame by spatial boundaries and query tags.
683
+
684
+ If the `polygon` and `tags` arguments are empty objects, the final
685
+ GeoDataFrame will not be filtered accordingly.
686
+
687
+ Parameters
688
+ ----------
689
+ gdf
690
+ Original GeoDataFrame of features.
691
+ polygon
692
+ If not empty, the spatial boundaries to filter the GeoDataFrame.
693
+ tags
694
+ If not empty, the query tags to filter the GeoDataFrame.
695
+
696
+ Returns
697
+ -------
698
+ gdf
699
+ Filtered GeoDataFrame of features.
700
+ """
701
+ # remove any null or empty geometries then fix any invalid geometries
702
+ gdf = gdf[~(gdf["geometry"].isna() | gdf["geometry"].is_empty)]
703
+ gdf.loc[:, "geometry"] = gdf["geometry"].make_valid()
704
+
705
+ # retain rows with geometries that intersect the polygon
706
+ if polygon.is_empty:
707
+ geom_filter = pd.Series(data=True, index=gdf.index)
708
+ else:
709
+ idx = utils_geo._intersect_index_quadrats(gdf["geometry"], polygon)
710
+ geom_filter = gdf.index.isin(idx)
711
+
712
+ # retain rows that have any of their tag filters satisfied
713
+ if len(tags) == 0:
714
+ tags_filter = pd.Series(data=True, index=gdf.index)
715
+ else:
716
+ tags_filter = pd.Series(data=False, index=gdf.index)
717
+ for col in set(gdf.columns) & tags.keys():
718
+ value = tags[col]
719
+ if value is True:
720
+ tags_filter |= gdf[col].notna()
721
+ elif isinstance(value, str):
722
+ tags_filter |= gdf[col] == value
723
+ elif isinstance(value, list):
724
+ tags_filter |= gdf[col].isin(set(value))
725
+
726
+ # filter gdf then drop any columns with only nulls left after filtering
727
+ gdf = gdf[geom_filter & tags_filter].dropna(axis="columns", how="all")
728
+ if len(gdf) == 0: # pragma: no cover
729
+ msg = "No matching features. Check query location, tags, and log."
730
+ raise InsufficientResponseError(msg)
731
+
732
+ msg = f"{len(gdf):,} features in the final GeoDataFrame"
733
+ utils.log(msg, level=lg.INFO)
734
+ return gdf
osmnx/source/osmnx/geocoder.py ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Geocode place names or addresses or retrieve OSM elements by place name or ID.
3
+
4
+ This module uses the Nominatim API's "search" and "lookup" endpoints. For more
5
+ details see https://wiki.openstreetmap.org/wiki/Elements and
6
+ https://nominatim.org/.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging as lg
12
+ from collections import OrderedDict
13
+ from typing import Any
14
+
15
+ import geopandas as gpd
16
+ import pandas as pd
17
+
18
+ from . import _nominatim
19
+ from . import settings
20
+ from . import utils
21
+ from ._errors import InsufficientResponseError
22
+
23
+
24
+ def geocode(query: str) -> tuple[float, float]:
25
+ """
26
+ Geocode place names or addresses to `(lat, lon)` with the Nominatim API.
27
+
28
+ This geocodes the query via the Nominatim "search" endpoint.
29
+
30
+ Parameters
31
+ ----------
32
+ query
33
+ The query string to geocode.
34
+
35
+ Returns
36
+ -------
37
+ point
38
+ The `(lat, lon)` coordinates returned by the geocoder.
39
+ """
40
+ # define the parameters
41
+ params: OrderedDict[str, int | str] = OrderedDict()
42
+ params["format"] = "json"
43
+ params["limit"] = 1
44
+ params["dedupe"] = 0 # prevent deduping to get precise number of results
45
+ params["q"] = query
46
+ response_json = _nominatim._nominatim_request(params=params)
47
+
48
+ # if results were returned, parse lat and lon out of the result
49
+ if response_json and "lat" in response_json[0] and "lon" in response_json[0]:
50
+ lat = float(response_json[0]["lat"])
51
+ lon = float(response_json[0]["lon"])
52
+ point = (lat, lon)
53
+
54
+ msg = f"Geocoded {query!r} to {point}"
55
+ utils.log(msg, level=lg.INFO)
56
+ return point
57
+
58
+ # otherwise we got no results back
59
+ msg = f"Nominatim could not geocode query {query!r}."
60
+ raise InsufficientResponseError(msg)
61
+
62
+
63
+ def geocode_to_gdf(
64
+ query: str | dict[str, str] | list[str | dict[str, str]],
65
+ *,
66
+ which_result: int | None | list[int | None] = None,
67
+ by_osmid: bool = False,
68
+ ) -> gpd.GeoDataFrame:
69
+ """
70
+ Retrieve OSM elements by place name or OSM ID with the Nominatim API.
71
+
72
+ If searching by place name, the `query` argument can be a string or
73
+ structured dict, or a list of such strings/dicts to send to the geocoder.
74
+ This uses the Nominatim "search" endpoint to geocode the place name to the
75
+ best-matching OSM element, then returns that element and its attribute
76
+ data.
77
+
78
+ You can instead query by OSM ID by passing `by_osmid=True`. This uses the
79
+ Nominatim "lookup" endpoint to retrieve the OSM element with that ID. In
80
+ this case, the function treats the `query` argument as an OSM ID (or list
81
+ of OSM IDs), which must be prepended with their types: node (N), way (W),
82
+ or relation (R) in accordance with the Nominatim API format. For example,
83
+ `query=["R2192363", "N240109189", "W427818536"]`.
84
+
85
+ If `query` is a list, then `which_result` must be either an int or a list
86
+ with the same length as `query`. The queries you provide must be
87
+ resolvable to elements in the Nominatim database. The resulting
88
+ GeoDataFrame's geometry column contains place boundaries if they exist.
89
+
90
+ Parameters
91
+ ----------
92
+ query
93
+ The query string(s) or structured dict(s) to geocode.
94
+ which_result
95
+ Which search result to return. If None, auto-select the first
96
+ (Multi)Polygon or raise an error if OSM doesn't return one. To get
97
+ the top match (sorted by importance) regardless of geometry type, set
98
+ `which_result=1`. Ignored if `by_osmid=True`.
99
+ by_osmid
100
+ If True, treat query as an OSM ID lookup rather than text search.
101
+
102
+ Returns
103
+ -------
104
+ gdf
105
+ GeoDataFrame with one row for each query result.
106
+ """
107
+ if isinstance(query, list):
108
+ # if query is a list of queries but which_result is int/None, then
109
+ # turn which_result into a list with same length as query list
110
+ q_list = query
111
+ wr_list = which_result if isinstance(which_result, list) else [which_result] * len(query)
112
+ else:
113
+ # if query is not already a list, turn it into one
114
+ # if which_result was a list, take 0th element, otherwise make it list
115
+ q_list = [query]
116
+ wr_list = [which_result[0]] if isinstance(which_result, list) else [which_result]
117
+
118
+ # ensure same length
119
+ if len(q_list) != len(wr_list): # pragma: no cover
120
+ msg = "`which_result` length must equal `query` length."
121
+ raise ValueError(msg)
122
+
123
+ # geocode each query, concat as GeoDataFrame rows, then set the CRS
124
+ results = (
125
+ _geocode_query_to_gdf(q, wr, by_osmid) for q, wr in zip(q_list, wr_list, strict=True)
126
+ )
127
+ gdf = pd.concat(results, ignore_index=True).set_crs(settings.default_crs)
128
+
129
+ msg = f"Created GeoDataFrame with {len(gdf)} rows from {len(q_list)} queries"
130
+ utils.log(msg, level=lg.INFO)
131
+ return gdf
132
+
133
+
134
+ def _geocode_query_to_gdf(
135
+ query: str | dict[str, str],
136
+ which_result: int | None,
137
+ by_osmid: bool, # noqa: FBT001
138
+ ) -> gpd.GeoDataFrame:
139
+ """
140
+ Geocode a single place query to a GeoDataFrame.
141
+
142
+ Parameters
143
+ ----------
144
+ query
145
+ Query string or structured dict to geocode.
146
+ which_result
147
+ Which search result to return. If None, auto-select the first
148
+ (Multi)Polygon or raise an error if OSM doesn't return one. To get
149
+ the top match regardless of geometry type, set `which_result=1`.
150
+ Ignored if `by_osmid=True`.
151
+ by_osmid
152
+ If True, treat query as an OSM ID lookup rather than text search.
153
+
154
+ Returns
155
+ -------
156
+ gdf
157
+ GeoDataFrame with one row containing the geocoding result.
158
+ """
159
+ limit = 50 if which_result is None else which_result
160
+ results = _nominatim._download_nominatim_element(query, by_osmid=by_osmid, limit=limit)
161
+
162
+ # ensure geocoder results are sorted from most to least important
163
+ results = sorted(results, key=lambda x: x["importance"], reverse=True)
164
+
165
+ # choose the right result from the JSON response
166
+ if len(results) == 0:
167
+ # if no results were returned, raise error
168
+ msg = f"Nominatim geocoder returned 0 results for query {query!r}."
169
+ raise InsufficientResponseError(msg)
170
+
171
+ if by_osmid:
172
+ # if searching by OSM ID, always take the first (ie, only) result
173
+ result = results[0]
174
+
175
+ elif which_result is None:
176
+ # else, if which_result=None, auto-select the first (Multi)Polygon
177
+ try:
178
+ result = _get_first_polygon(results)
179
+ except TypeError as e:
180
+ msg = f"Nominatim did not geocode query {query!r} to a geometry of type (Multi)Polygon."
181
+ raise TypeError(msg) from e
182
+
183
+ elif len(results) >= which_result:
184
+ # else, if we got at least which_result results, choose that one
185
+ result = results[which_result - 1]
186
+
187
+ else: # pragma: no cover
188
+ # else, we got fewer results than which_result, raise error
189
+ msg = f"Nominatim returned {len(results)} result(s) but `which_result={which_result}`."
190
+ raise InsufficientResponseError(msg)
191
+
192
+ # if we got a non (Multi)Polygon geometry type (like a point), log warning
193
+ geom_type = result["geojson"]["type"]
194
+ if geom_type not in {"Polygon", "MultiPolygon"}:
195
+ msg = f"Nominatim geocoder returned a {geom_type} as the geometry for query {query!r}"
196
+ utils.log(msg, level=lg.WARNING)
197
+
198
+ # build the GeoJSON feature from the chosen result
199
+ bottom, top, left, right = result["boundingbox"]
200
+ feature = {
201
+ "type": "Feature",
202
+ "geometry": result["geojson"],
203
+ "properties": {
204
+ "bbox_west": left,
205
+ "bbox_south": bottom,
206
+ "bbox_east": right,
207
+ "bbox_north": top,
208
+ },
209
+ }
210
+
211
+ # add the other attributes we retrieved
212
+ for attr in result:
213
+ if attr not in {"address", "boundingbox", "geojson", "icon", "licence"}:
214
+ feature["properties"][attr] = result[attr]
215
+
216
+ # create and return the GeoDataFrame
217
+ gdf = gpd.GeoDataFrame.from_features([feature])
218
+ cols = ["lat", "lon", "bbox_north", "bbox_south", "bbox_east", "bbox_west"]
219
+ gdf[cols] = gdf[cols].astype(float)
220
+ return gdf
221
+
222
+
223
+ def _get_first_polygon(results: list[dict[str, Any]]) -> dict[str, Any]:
224
+ """
225
+ Choose first result of geometry type (Multi)Polygon from list of results.
226
+
227
+ Parameters
228
+ ----------
229
+ results
230
+ Results from the Nominatim API.
231
+
232
+ Returns
233
+ -------
234
+ result
235
+ The chosen result.
236
+ """
237
+ polygon_types = {"Polygon", "MultiPolygon"}
238
+
239
+ for result in results:
240
+ if "geojson" in result and result["geojson"]["type"] in polygon_types:
241
+ return result
242
+
243
+ # if we never found a polygon, raise an error
244
+ raise TypeError
osmnx/source/osmnx/graph.py ADDED
@@ -0,0 +1,863 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Download and create graphs from OpenStreetMap data.
3
+
4
+ Refer to the Getting Started guide for usage limitations.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging as lg
10
+ from collections.abc import Iterable
11
+ from importlib.metadata import version as metadata_version
12
+ from itertools import groupby
13
+ from itertools import pairwise
14
+ from pathlib import Path
15
+ from typing import TYPE_CHECKING
16
+ from typing import Any
17
+
18
+ import networkx as nx
19
+ from shapely import MultiPolygon
20
+ from shapely import Polygon
21
+
22
+ from . import _osm_xml
23
+ from . import _overpass
24
+ from . import distance
25
+ from . import geocoder
26
+ from . import projection
27
+ from . import settings
28
+ from . import simplification
29
+ from . import stats
30
+ from . import truncate
31
+ from . import utils
32
+ from . import utils_geo
33
+ from ._errors import CacheOnlyInterruptError
34
+ from ._errors import InsufficientResponseError
35
+
36
+ if TYPE_CHECKING:
37
+ from collections.abc import Iterable
38
+
39
+
40
+ def graph_from_bbox(
41
+ bbox: tuple[float, float, float, float],
42
+ *,
43
+ network_type: str = "all",
44
+ simplify: bool = True,
45
+ retain_all: bool = False,
46
+ truncate_by_edge: bool = False,
47
+ custom_filter: str | list[str] | None = None,
48
+ ) -> nx.MultiDiGraph:
49
+ """
50
+ Download and create a graph within a lat-lon bounding box.
51
+
52
+ This function uses filters to query the Overpass API: you can either
53
+ specify a pre-defined `network_type` or provide your own `custom_filter`
54
+ with Overpass QL.
55
+
56
+ Use the `settings` module's `useful_tags_node` and `useful_tags_way`
57
+ settings to configure which OSM node/way tags are added as graph node/edge
58
+ attributes. If you want a fully bidirectional network, ensure your
59
+ `network_type` is in `settings.bidirectional_network_types` before
60
+ creating your graph. You can also use the `settings` module to retrieve a
61
+ snapshot of historical OSM data as of a certain date, or to configure the
62
+ Overpass server timeout, memory allocation, and other customizations.
63
+
64
+ Parameters
65
+ ----------
66
+ bbox
67
+ Bounding box as `(left, bottom, right, top)`. Coordinates should be in
68
+ unprojected latitude-longitude degrees (EPSG:4326).
69
+ network_type
70
+ {"all", "all_public", "bike", "drive", "drive_service", "walk"}
71
+ What type of street network to retrieve if `custom_filter` is None.
72
+ simplify
73
+ If True, simplify graph topology via the `simplify_graph` function.
74
+ retain_all
75
+ If True, return the entire graph even if it is not connected. If
76
+ False, retain only the largest weakly connected component.
77
+ truncate_by_edge
78
+ If True, retain nodes the outside bounding box if at least one of
79
+ the node's neighbors lies within the bounding box.
80
+ custom_filter
81
+ A custom ways filter to be used instead of the `network_type` presets,
82
+ e.g. `'["power"~"line"]' or '["highway"~"motorway|trunk"]'`. If `str`,
83
+ the intersection of keys/values will be used, e.g., `'[maxspeed=50][lanes=2]'`
84
+ will return all ways having both maxspeed of 50 and two lanes. If
85
+ `list`, the union of the `list` items will be used, e.g.,
86
+ `['[maxspeed=50]', '[lanes=2]']` will return all ways having either
87
+ maximum speed of 50 or two lanes. Also pass in a `network_type` that
88
+ is in `settings.bidirectional_network_types` if you want the graph to
89
+ be fully bidirectional.
90
+
91
+ Returns
92
+ -------
93
+ G
94
+ The resulting MultiDiGraph.
95
+
96
+ Notes
97
+ -----
98
+ Very large query areas use the `utils_geo._consolidate_subdivide_geometry`
99
+ function to automatically make multiple requests: see that function's
100
+ documentation for caveats.
101
+ """
102
+ # convert bounding box to a polygon
103
+ polygon = utils_geo.bbox_to_poly(bbox)
104
+
105
+ # create graph using this polygon geometry
106
+ G = graph_from_polygon(
107
+ polygon,
108
+ network_type=network_type,
109
+ simplify=simplify,
110
+ retain_all=retain_all,
111
+ truncate_by_edge=truncate_by_edge,
112
+ custom_filter=custom_filter,
113
+ )
114
+
115
+ msg = f"graph_from_bbox returned graph with {len(G):,} nodes and {len(G.edges):,} edges"
116
+ utils.log(msg, level=lg.INFO)
117
+ return G
118
+
119
+
120
+ def graph_from_point(
121
+ center_point: tuple[float, float],
122
+ dist: float,
123
+ *,
124
+ dist_type: str = "bbox",
125
+ network_type: str = "all",
126
+ simplify: bool = True,
127
+ retain_all: bool = False,
128
+ truncate_by_edge: bool = False,
129
+ custom_filter: str | list[str] | None = None,
130
+ ) -> nx.MultiDiGraph:
131
+ """
132
+ Download and create a graph within some distance of a lat-lon point.
133
+
134
+ This function uses filters to query the Overpass API: you can either
135
+ specify a pre-defined `network_type` or provide your own `custom_filter`
136
+ with Overpass QL.
137
+
138
+ Use the `settings` module's `useful_tags_node` and `useful_tags_way`
139
+ settings to configure which OSM node/way tags are added as graph node/edge
140
+ attributes. If you want a fully bidirectional network, ensure your
141
+ `network_type` is in `settings.bidirectional_network_types` before
142
+ creating your graph. You can also use the `settings` module to retrieve a
143
+ snapshot of historical OSM data as of a certain date, or to configure the
144
+ Overpass server timeout, memory allocation, and other customizations.
145
+
146
+ Parameters
147
+ ----------
148
+ center_point
149
+ The `(lat, lon)` center point around which to construct the graph.
150
+ Coordinates should be in unprojected latitude-longitude degrees
151
+ (EPSG:4326).
152
+ dist
153
+ Retain only those nodes within this many meters of `center_point`,
154
+ measuring distance according to `dist_type`.
155
+ dist_type
156
+ {"bbox", "network"}
157
+ If "bbox", retain only those nodes within a bounding box of `dist`
158
+ length/width. If "network", retain only those nodes within `dist`
159
+ network distance of the nearest node to `center_point`.
160
+ network_type
161
+ {"all", "all_public", "bike", "drive", "drive_service", "walk"}
162
+ What type of street network to retrieve if `custom_filter` is None.
163
+ simplify
164
+ If True, simplify graph topology with the `simplify_graph` function.
165
+ retain_all
166
+ If True, return the entire graph even if it is not connected. If
167
+ False, retain only the largest weakly connected component.
168
+ truncate_by_edge
169
+ If True, retain nodes the outside bounding box if at least one of
170
+ the node's neighbors lies within the bounding box.
171
+ custom_filter
172
+ A custom ways filter to be used instead of the `network_type` presets,
173
+ e.g. `'["power"~"line"]' or '["highway"~"motorway|trunk"]'`. If `str`,
174
+ the intersection of keys/values will be used, e.g., `'[maxspeed=50][lanes=2]'`
175
+ will return all ways having both maxspeed of 50 and two lanes. If
176
+ `list`, the union of the `list` items will be used, e.g.,
177
+ `['[maxspeed=50]', '[lanes=2]']` will return all ways having either
178
+ maximum speed of 50 or two lanes. Also pass in a `network_type` that
179
+ is in `settings.bidirectional_network_types` if you want the graph to
180
+ be fully bidirectional.
181
+
182
+ Returns
183
+ -------
184
+ G
185
+ The resulting MultiDiGraph.
186
+
187
+ Notes
188
+ -----
189
+ Very large query areas use the `utils_geo._consolidate_subdivide_geometry`
190
+ function to automatically make multiple requests: see that function's
191
+ documentation for caveats.
192
+ """
193
+ if dist_type not in {"bbox", "network"}: # pragma: no cover
194
+ msg = "`dist_type` must be 'bbox' or 'network'."
195
+ raise ValueError(msg)
196
+
197
+ # create bounding box from center point and distance in each direction
198
+ bbox = utils_geo.bbox_from_point(center_point, dist)
199
+
200
+ # create a graph from the bounding box
201
+ G = graph_from_bbox(
202
+ bbox,
203
+ network_type=network_type,
204
+ simplify=simplify,
205
+ retain_all=retain_all,
206
+ truncate_by_edge=truncate_by_edge,
207
+ custom_filter=custom_filter,
208
+ )
209
+
210
+ if dist_type == "network":
211
+ # find node nearest to center then truncate graph by dist from it
212
+ node = distance.nearest_nodes(G, X=center_point[1], Y=center_point[0])
213
+ G = truncate.truncate_graph_dist(G, node, dist)
214
+
215
+ msg = f"graph_from_point returned graph with {len(G):,} nodes and {len(G.edges):,} edges"
216
+ utils.log(msg, level=lg.INFO)
217
+ return G
218
+
219
+
220
+ def graph_from_address(
221
+ address: str,
222
+ dist: float,
223
+ *,
224
+ dist_type: str = "bbox",
225
+ network_type: str = "all",
226
+ simplify: bool = True,
227
+ retain_all: bool = False,
228
+ truncate_by_edge: bool = False,
229
+ custom_filter: str | list[str] | None = None,
230
+ ) -> nx.MultiDiGraph:
231
+ """
232
+ Download and create a graph within some distance of an address.
233
+
234
+ This function uses filters to query the Overpass API: you can either
235
+ specify a pre-defined `network_type` or provide your own `custom_filter`
236
+ with Overpass QL.
237
+
238
+ Use the `settings` module's `useful_tags_node` and `useful_tags_way`
239
+ settings to configure which OSM node/way tags are added as graph node/edge
240
+ attributes. If you want a fully bidirectional network, ensure your
241
+ `network_type` is in `settings.bidirectional_network_types` before
242
+ creating your graph. You can also use the `settings` module to retrieve a
243
+ snapshot of historical OSM data as of a certain date, or to configure the
244
+ Overpass server timeout, memory allocation, and other customizations.
245
+
246
+ Parameters
247
+ ----------
248
+ address
249
+ The address to geocode and use as the central point around which to
250
+ construct the graph.
251
+ dist
252
+ Retain only those nodes within this many meters of `center_point`,
253
+ measuring distance according to `dist_type`.
254
+ dist_type
255
+ {"network", "bbox"}
256
+ If "bbox", retain only those nodes within a bounding box of `dist`. If
257
+ "network", retain only those nodes within `dist` network distance from
258
+ the centermost node.
259
+ network_type
260
+ {"all", "all_public", "bike", "drive", "drive_service", "walk"}
261
+ What type of street network to retrieve if `custom_filter` is None.
262
+ simplify
263
+ If True, simplify graph topology with the `simplify_graph` function.
264
+ retain_all
265
+ If True, return the entire graph even if it is not connected. If
266
+ False, retain only the largest weakly connected component.
267
+ truncate_by_edge
268
+ If True, retain nodes the outside bounding box if at least one of
269
+ the node's neighbors lies within the bounding box.
270
+ custom_filter
271
+ A custom ways filter to be used instead of the `network_type` presets,
272
+ e.g. `'["power"~"line"]' or '["highway"~"motorway|trunk"]'`. If `str`,
273
+ the intersection of keys/values will be used, e.g., `'[maxspeed=50][lanes=2]'`
274
+ will return all ways having both maxspeed of 50 and two lanes. If
275
+ `list`, the union of the `list` items will be used, e.g.,
276
+ `['[maxspeed=50]', '[lanes=2]']` will return all ways having either
277
+ maximum speed of 50 or two lanes. Also pass in a `network_type` that
278
+ is in `settings.bidirectional_network_types` if you want the graph to
279
+ be fully bidirectional.
280
+
281
+ Returns
282
+ -------
283
+ G
284
+ The resulting MultiDiGraph.
285
+
286
+ Notes
287
+ -----
288
+ Very large query areas use the `utils_geo._consolidate_subdivide_geometry`
289
+ function to automatically make multiple requests: see that function's
290
+ documentation for caveats.
291
+ """
292
+ # geocode the address string to a (lat, lon) point
293
+ point = geocoder.geocode(address)
294
+
295
+ # then create a graph from this point
296
+ G = graph_from_point(
297
+ point,
298
+ dist,
299
+ dist_type=dist_type,
300
+ network_type=network_type,
301
+ simplify=simplify,
302
+ retain_all=retain_all,
303
+ truncate_by_edge=truncate_by_edge,
304
+ custom_filter=custom_filter,
305
+ )
306
+
307
+ msg = f"graph_from_address returned graph with {len(G):,} nodes and {len(G.edges):,} edges"
308
+ utils.log(msg, level=lg.INFO)
309
+ return G
310
+
311
+
312
+ def graph_from_place(
313
+ query: str | dict[str, str] | list[str | dict[str, str]],
314
+ *,
315
+ network_type: str = "all",
316
+ simplify: bool = True,
317
+ retain_all: bool = False,
318
+ truncate_by_edge: bool = False,
319
+ which_result: int | None | list[int | None] = None,
320
+ custom_filter: str | list[str] | None = None,
321
+ ) -> nx.MultiDiGraph:
322
+ """
323
+ Download and create a graph within the boundaries of some place(s).
324
+
325
+ The query must be geocodable and OSM must have polygon boundaries for the
326
+ geocode result. If OSM does not have a polygon for this place, you can
327
+ instead get its street network using the `graph_from_address` function,
328
+ which geocodes the place name to a point and gets the network within some
329
+ distance of that point.
330
+
331
+ If OSM does have polygon boundaries for this place but you're not finding
332
+ it, try to vary the query string, pass in a structured query dict, or vary
333
+ the `which_result` argument to use a different geocode result. If you know
334
+ the OSM ID of the place, you can retrieve its boundary polygon using the
335
+ `geocode_to_gdf` function, then pass it to the `features_from_polygon`
336
+ function.
337
+
338
+ This function uses filters to query the Overpass API: you can either
339
+ specify a pre-defined `network_type` or provide your own `custom_filter`
340
+ with Overpass QL.
341
+
342
+ Use the `settings` module's `useful_tags_node` and `useful_tags_way`
343
+ settings to configure which OSM node/way tags are added as graph node/edge
344
+ attributes. If you want a fully bidirectional network, ensure your
345
+ `network_type` is in `settings.bidirectional_network_types` before
346
+ creating your graph. You can also use the `settings` module to retrieve a
347
+ snapshot of historical OSM data as of a certain date, or to configure the
348
+ Overpass server timeout, memory allocation, and other customizations.
349
+
350
+ Parameters
351
+ ----------
352
+ query
353
+ The query or queries to geocode to retrieve place boundary polygon(s).
354
+ network_type
355
+ {"all", "all_public", "bike", "drive", "drive_service", "walk"}
356
+ What type of street network to retrieve if `custom_filter` is None.
357
+ simplify
358
+ If True, simplify graph topology with the `simplify_graph` function.
359
+ retain_all
360
+ If True, return the entire graph even if it is not connected. If
361
+ False, retain only the largest weakly connected component.
362
+ truncate_by_edge
363
+ If True, retain nodes outside the place boundary polygon(s) if at
364
+ least one of the node's neighbors lies within the polygon(s).
365
+ which_result
366
+ Which geocoding result to use. if None, auto-select the first
367
+ (Multi)Polygon or raise an error if OSM doesn't return one.
368
+ custom_filter
369
+ A custom ways filter to be used instead of the `network_type` presets,
370
+ e.g. `'["power"~"line"]' or '["highway"~"motorway|trunk"]'`. If `str`,
371
+ the intersection of keys/values will be used, e.g., `'[maxspeed=50][lanes=2]'`
372
+ will return all ways having both maxspeed of 50 and two lanes. If
373
+ `list`, the union of the `list` items will be used, e.g.,
374
+ `['[maxspeed=50]', '[lanes=2]']` will return all ways having either
375
+ maximum speed of 50 or two lanes. Also pass in a `network_type` that
376
+ is in `settings.bidirectional_network_types` if you want the graph to
377
+ be fully bidirectional.
378
+
379
+ Returns
380
+ -------
381
+ G
382
+ The resulting MultiDiGraph.
383
+
384
+ Notes
385
+ -----
386
+ Very large query areas use the `utils_geo._consolidate_subdivide_geometry`
387
+ function to automatically make multiple requests: see that function's
388
+ documentation for caveats.
389
+ """
390
+ # extract the geometry from the GeoDataFrame to use in query
391
+ polygon = geocoder.geocode_to_gdf(query, which_result=which_result).union_all()
392
+ msg = "Constructed place geometry polygon(s) to query Overpass"
393
+ utils.log(msg, level=lg.INFO)
394
+
395
+ # create graph using this polygon(s) geometry
396
+ G = graph_from_polygon(
397
+ polygon,
398
+ network_type=network_type,
399
+ simplify=simplify,
400
+ retain_all=retain_all,
401
+ truncate_by_edge=truncate_by_edge,
402
+ custom_filter=custom_filter,
403
+ )
404
+
405
+ msg = f"graph_from_place returned graph with {len(G):,} nodes and {len(G.edges):,} edges"
406
+ utils.log(msg, level=lg.INFO)
407
+ return G
408
+
409
+
410
+ def graph_from_polygon(
411
+ polygon: Polygon | MultiPolygon,
412
+ *,
413
+ network_type: str = "all",
414
+ simplify: bool = True,
415
+ retain_all: bool = False,
416
+ truncate_by_edge: bool = False,
417
+ custom_filter: str | list[str] | None = None,
418
+ ) -> nx.MultiDiGraph:
419
+ """
420
+ Download and create a graph within the boundaries of a (Multi)Polygon.
421
+
422
+ This function uses filters to query the Overpass API: you can either
423
+ specify a pre-defined `network_type` or provide your own `custom_filter`
424
+ with Overpass QL.
425
+
426
+ Use the `settings` module's `useful_tags_node` and `useful_tags_way`
427
+ settings to configure which OSM node/way tags are added as graph node/edge
428
+ attributes. If you want a fully bidirectional network, ensure your
429
+ `network_type` is in `settings.bidirectional_network_types` before
430
+ creating your graph. You can also use the `settings` module to retrieve a
431
+ snapshot of historical OSM data as of a certain date, or to configure the
432
+ Overpass server timeout, memory allocation, and other customizations.
433
+
434
+ Parameters
435
+ ----------
436
+ polygon
437
+ The geometry within which to construct the graph. Coordinates should
438
+ be in unprojected latitude-longitude degrees (EPSG:4326).
439
+ network_type
440
+ {"all", "all_public", "bike", "drive", "drive_service", "walk"}
441
+ What type of street network to retrieve if `custom_filter` is None.
442
+ simplify
443
+ If True, simplify graph topology with the `simplify_graph` function.
444
+ retain_all
445
+ If True, return the entire graph even if it is not connected. If
446
+ False, retain only the largest weakly connected component.
447
+ truncate_by_edge
448
+ If True, retain nodes outside `polygon` if at least one of the node's
449
+ neighbors lies within `polygon`.
450
+ custom_filter
451
+ A custom ways filter to be used instead of the `network_type` presets,
452
+ e.g. `'["power"~"line"]' or '["highway"~"motorway|trunk"]'`. If `str`,
453
+ the intersection of keys/values will be used, e.g., `'[maxspeed=50][lanes=2]'`
454
+ will return all ways having both maxspeed of 50 and two lanes. If
455
+ `list`, the union of the `list` items will be used, e.g.,
456
+ `['[maxspeed=50]', '[lanes=2]']` will return all ways having either
457
+ maximum speed of 50 or two lanes. Also pass in a `network_type` that
458
+ is in `settings.bidirectional_network_types` if you want the graph to
459
+ be fully bidirectional.
460
+
461
+ Returns
462
+ -------
463
+ G
464
+ The resulting MultiDiGraph.
465
+
466
+ Notes
467
+ -----
468
+ Very large query areas use the `utils_geo._consolidate_subdivide_geometry`
469
+ function to automatically make multiple requests: see that function's
470
+ documentation for caveats.
471
+ """
472
+ # verify that the geometry is valid and is a shapely Polygon/MultiPolygon
473
+ # before proceeding
474
+ if not polygon.is_valid: # pragma: no cover
475
+ msg = "The geometry of `polygon` is invalid."
476
+ raise ValueError(msg)
477
+ if not isinstance(polygon, (Polygon, MultiPolygon)): # pragma: no cover
478
+ msg = (
479
+ "Geometry must be a shapely Polygon or MultiPolygon. If you "
480
+ "requested graph from place name, make sure your query resolves "
481
+ "to a Polygon or MultiPolygon, and not some other geometry, like "
482
+ "a Point. See OSMnx documentation for details."
483
+ )
484
+ raise TypeError(msg)
485
+
486
+ # create a new buffered polygon 0.5km around the desired one
487
+ poly_proj, crs_utm = projection.project_geometry(polygon)
488
+ poly_proj_buff = poly_proj.buffer(500)
489
+ poly_buff, _ = projection.project_geometry(poly_proj_buff, crs=crs_utm, to_latlong=True)
490
+
491
+ # download the network data from OSM within buffered polygon
492
+ response_jsons = _overpass._download_overpass_network(poly_buff, network_type, custom_filter)
493
+
494
+ # create buffered graph from the downloaded data
495
+ bidirectional = network_type in settings.bidirectional_network_types
496
+ G_buff = _create_graph(response_jsons, bidirectional)
497
+
498
+ # truncate buffered graph to the buffered polygon and retain_all for
499
+ # now. needed because overpass returns entire ways that also include
500
+ # nodes outside the poly if the way (that is, a way with a single OSM
501
+ # ID) has a node inside the poly at some point.
502
+ G_buff = truncate.truncate_graph_polygon(G_buff, poly_buff, truncate_by_edge=truncate_by_edge)
503
+
504
+ # keep only the largest weakly connected component if retain_all is False
505
+ if not retain_all:
506
+ G_buff = truncate.largest_component(G_buff, strongly=False)
507
+
508
+ # simplify the graph topology
509
+ if simplify:
510
+ G_buff = simplification.simplify_graph(G_buff)
511
+
512
+ # truncate graph by original polygon to return graph within polygon
513
+ # caller wants. don't simplify again: this allows us to retain
514
+ # intersections along the street that may now only connect 2 street
515
+ # segments in the network, but in reality also connect to an
516
+ # intersection just outside the polygon
517
+ G = truncate.truncate_graph_polygon(G_buff, polygon, truncate_by_edge=truncate_by_edge)
518
+
519
+ # keep only the largest weakly connected component if retain_all is False
520
+ # we're doing this again in case the last truncate disconnected anything
521
+ # on the periphery
522
+ if not retain_all:
523
+ G = truncate.largest_component(G, strongly=False)
524
+
525
+ # count how many physical streets in buffered graph connect to each
526
+ # intersection in un-buffered graph, to retain true counts for each
527
+ # intersection, even if some of its neighbors are outside the polygon
528
+ spn = stats.count_streets_per_node(G_buff, nodes=G.nodes)
529
+ nx.set_node_attributes(G, values=spn, name="street_count")
530
+
531
+ msg = f"graph_from_polygon returned graph with {len(G):,} nodes and {len(G.edges):,} edges"
532
+ utils.log(msg, level=lg.INFO)
533
+ return G
534
+
535
+
536
+ def graph_from_xml(
537
+ filepath: str | Path,
538
+ *,
539
+ bidirectional: bool = False,
540
+ simplify: bool = True,
541
+ retain_all: bool = False,
542
+ encoding: str = "utf-8",
543
+ ) -> nx.MultiDiGraph:
544
+ """
545
+ Create a graph from data in an OSM XML file.
546
+
547
+ Do not load an XML file previously generated by OSMnx: this use case is
548
+ not supported and may not behave as expected. To save/load graphs to/from
549
+ disk for later use in OSMnx, use the `io.save_graphml` and
550
+ `io.load_graphml` functions instead.
551
+
552
+ Use the `settings` module's `useful_tags_node` and `useful_tags_way`
553
+ settings to configure which OSM node/way tags are added as graph node/edge
554
+ attributes.
555
+
556
+ Parameters
557
+ ----------
558
+ filepath
559
+ Path to file containing OSM XML data.
560
+ bidirectional
561
+ If True, create bidirectional edges for one-way streets.
562
+ simplify
563
+ If True, simplify graph topology with the `simplify_graph` function.
564
+ retain_all
565
+ If True, return the entire graph even if it is not connected. If
566
+ False, retain only the largest weakly connected component.
567
+ encoding
568
+ The OSM XML file's character encoding.
569
+
570
+ Returns
571
+ -------
572
+ G
573
+ The resulting MultiDiGraph.
574
+ """
575
+ # transmogrify file of OSM XML data into JSON
576
+ response_jsons = [_osm_xml._overpass_json_from_xml(Path(filepath), encoding)]
577
+
578
+ # create graph using this response JSON
579
+ G = _create_graph(response_jsons, bidirectional)
580
+
581
+ # keep only the largest weakly connected component if retain_all is False
582
+ if not retain_all:
583
+ G = truncate.largest_component(G, strongly=False)
584
+
585
+ # simplify the graph topology as the last step
586
+ if simplify:
587
+ G = simplification.simplify_graph(G)
588
+
589
+ msg = f"graph_from_xml returned graph with {len(G):,} nodes and {len(G.edges):,} edges"
590
+ utils.log(msg, level=lg.INFO)
591
+ return G
592
+
593
+
594
+ def _create_graph(
595
+ response_jsons: Iterable[dict[str, Any]],
596
+ bidirectional: bool, # noqa: FBT001
597
+ ) -> nx.MultiDiGraph:
598
+ """
599
+ Create a NetworkX MultiDiGraph from Overpass API responses.
600
+
601
+ Adds length attributes in meters (great-circle distance between endpoints)
602
+ to all of the graph's (pre-simplified, straight-line) edges via the
603
+ `distance.add_edge_lengths` function.
604
+
605
+ Parameters
606
+ ----------
607
+ response_jsons
608
+ Iterable of JSON responses from the Overpass API.
609
+ bidirectional
610
+ If True, create bidirectional edges for one-way streets.
611
+
612
+ Returns
613
+ -------
614
+ G
615
+ The resulting MultiDiGraph.
616
+ """
617
+ # each dict's keys are OSM IDs and values are dicts of attributes
618
+ nodes: dict[int, dict[str, Any]] = {}
619
+ paths: dict[int, dict[str, Any]] = {}
620
+
621
+ # consume response_jsons generator to download data from server. if
622
+ # cache_only_mode, just consume response_jsons then continue next loop.
623
+ # otherwise, extract nodes and paths from the downloaded OSM data.
624
+ response_count = 0
625
+ for response_json in response_jsons:
626
+ response_count += 1
627
+ if not settings.cache_only_mode:
628
+ nodes_temp, paths_temp = _parse_nodes_paths(response_json)
629
+ nodes.update(nodes_temp)
630
+ paths.update(paths_temp)
631
+
632
+ msg = f"Retrieved all data from API in {response_count} request(s)"
633
+ utils.log(msg, level=lg.INFO)
634
+ if settings.cache_only_mode: # pragma: no cover
635
+ # after consuming all response_jsons in loop, raise exception to catch
636
+ msg = "Interrupted because `settings.cache_only_mode=True`."
637
+ raise CacheOnlyInterruptError(msg)
638
+
639
+ # ensure we got some node/way data back from the server request(s)
640
+ if (len(nodes) == 0) and (len(paths) == 0): # pragma: no cover
641
+ msg = "No data elements in server response. Check query location/filters and log."
642
+ raise InsufficientResponseError(msg)
643
+
644
+ # create the MultiDiGraph and set its graph-level attributes
645
+ metadata = {
646
+ "created_date": utils.ts(),
647
+ "created_with": f"OSMnx {metadata_version('osmnx')}",
648
+ "crs": settings.default_crs,
649
+ }
650
+ G = nx.MultiDiGraph(**metadata)
651
+
652
+ # add each OSM node and way (a path of edges) to the graph
653
+ msg = f"Creating graph from {len(nodes):,} OSM nodes and {len(paths):,} OSM ways..."
654
+ utils.log(msg, level=lg.INFO)
655
+ G.add_nodes_from(nodes.items())
656
+ _add_paths(G, paths.values(), bidirectional)
657
+
658
+ msg = f"Created graph with {len(G):,} nodes and {len(G.edges):,} edges"
659
+ utils.log(msg, level=lg.INFO)
660
+
661
+ # add length (great-circle distance between nodes) attribute to each edge
662
+ if len(G.edges) > 0:
663
+ G = distance.add_edge_lengths(G)
664
+
665
+ return G
666
+
667
+
668
+ def _convert_node(element: dict[str, Any]) -> dict[str, Any]:
669
+ """
670
+ Convert an OSM node element into the format for a NetworkX node.
671
+
672
+ Parameters
673
+ ----------
674
+ element
675
+ OSM element of type "node".
676
+
677
+ Returns
678
+ -------
679
+ node
680
+ The converted node.
681
+ """
682
+ node = {"y": element["lat"], "x": element["lon"]}
683
+ if "tags" in element:
684
+ for useful_tag in settings.useful_tags_node:
685
+ if useful_tag in element["tags"]:
686
+ node[useful_tag] = element["tags"][useful_tag]
687
+ return node
688
+
689
+
690
+ def _convert_path(element: dict[str, Any]) -> dict[str, Any]:
691
+ """
692
+ Convert an OSM way element into the format for a NetworkX path.
693
+
694
+ Parameters
695
+ ----------
696
+ element
697
+ OSM element of type "way".
698
+
699
+ Returns
700
+ -------
701
+ path
702
+ The converted path.
703
+ """
704
+ path = {"osmid": element["id"]}
705
+
706
+ # remove any consecutive duplicate elements in the list of nodes
707
+ path["nodes"] = [group[0] for group in groupby(element["nodes"])]
708
+
709
+ if "tags" in element:
710
+ for useful_tag in settings.useful_tags_way:
711
+ if useful_tag in element["tags"]:
712
+ path[useful_tag] = element["tags"][useful_tag]
713
+ return path
714
+
715
+
716
+ def _parse_nodes_paths(
717
+ response_json: dict[str, Any],
718
+ ) -> tuple[dict[int, dict[str, Any]], dict[int, dict[str, Any]]]:
719
+ """
720
+ Construct dicts of nodes and paths from an Overpass response.
721
+
722
+ Parameters
723
+ ----------
724
+ response_json
725
+ JSON response from the Overpass API.
726
+
727
+ Returns
728
+ -------
729
+ nodes, paths
730
+ Each dict's keys are OSM IDs and values are dicts of attributes.
731
+ """
732
+ nodes = {}
733
+ paths = {}
734
+ for element in response_json["elements"]:
735
+ if element["type"] == "node":
736
+ nodes[element["id"]] = _convert_node(element)
737
+ elif element["type"] == "way":
738
+ paths[element["id"]] = _convert_path(element)
739
+
740
+ return nodes, paths
741
+
742
+
743
+ def _is_path_one_way(attrs: dict[str, Any], bidirectional: bool, oneway_values: set[str]) -> bool: # noqa: FBT001
744
+ """
745
+ Determine if a path of nodes allows travel in only one direction.
746
+
747
+ Parameters
748
+ ----------
749
+ attrs
750
+ A path's `tag:value` attribute data.
751
+ bidirectional
752
+ Whether this is a bidirectional network type.
753
+ oneway_values
754
+ The values OSM uses in its "oneway" tag to denote True.
755
+
756
+ Returns
757
+ -------
758
+ is_one_way
759
+ True if path allows travel in only one direction, otherwise False.
760
+ """
761
+ # rule 1
762
+ if settings.all_oneway:
763
+ # if globally configured to set every edge one-way, then it's one-way
764
+ return True
765
+
766
+ # rule 2
767
+ if bidirectional:
768
+ # if this is a bidirectional network type, then nothing in it is
769
+ # considered one-way. eg, if this is a walking network, this may very
770
+ # well be a one-way street (as cars/bikes go), but in a walking-only
771
+ # network it is a bidirectional edge (you can walk both directions on
772
+ # a one-way street). so we will add this path (in both directions) to
773
+ # the graph and set its oneway attribute to False.
774
+ return False
775
+
776
+ # rule 3
777
+ if "oneway" in attrs and attrs["oneway"] in oneway_values:
778
+ # if this path is tagged as one-way and if it is not a bidirectional
779
+ # network type then we'll add the path in one direction only
780
+ return True
781
+
782
+ # rule 4
783
+ if "junction" in attrs and attrs["junction"] == "roundabout": # noqa: SIM103
784
+ # roundabouts are also one-way but are not explicitly tagged as such
785
+ return True
786
+
787
+ # otherwise, if no rule passed then this path is not tagged as a one-way
788
+ return False
789
+
790
+
791
+ def _is_path_reversed(attrs: dict[str, Any], reversed_values: set[str]) -> bool:
792
+ """
793
+ Determine if the order of nodes in a path should be reversed.
794
+
795
+ Parameters
796
+ ----------
797
+ attrs
798
+ A path's `tag:value` attribute data.
799
+ reversed_values
800
+ The values OSM uses in its 'oneway' tag to denote travel can only
801
+ occur in the opposite direction of the node order.
802
+
803
+ Returns
804
+ -------
805
+ is_reversed
806
+ True if nodes' order should be reversed, otherwise False.
807
+ """
808
+ return "oneway" in attrs and attrs["oneway"] in reversed_values
809
+
810
+
811
+ def _add_paths(
812
+ G: nx.MultiDiGraph,
813
+ paths: Iterable[dict[str, Any]],
814
+ bidirectional: bool, # noqa: FBT001
815
+ ) -> None:
816
+ """
817
+ Add OSM paths to the graph as edges.
818
+
819
+ Parameters
820
+ ----------
821
+ G
822
+ The graph to add paths to.
823
+ paths
824
+ Iterable of paths' `tag:value` attribute data dicts.
825
+ bidirectional
826
+ If True, create bidirectional edges for one-way streets.
827
+ """
828
+ # the values OSM uses in its 'oneway' tag to denote True, and to denote
829
+ # travel can only occur in the opposite direction of the node order. see:
830
+ # https://wiki.openstreetmap.org/wiki/Key:oneway
831
+ # https://www.geofabrik.de/de/data/geofabrik-osm-gis-standard-0.7.pdf
832
+ oneway_values = {"yes", "true", "1", "-1", "reverse", "T", "F"}
833
+ reversed_values = {"-1", "reverse", "T"}
834
+
835
+ for path in paths:
836
+ # extract/remove the ordered list of nodes from this path element so
837
+ # we don't add it as a superfluous attribute to the edge later
838
+ nodes = path.pop("nodes")
839
+
840
+ # reverse the order of nodes in the path if this path is both one-way
841
+ # and only allows travel in the opposite direction of nodes' order
842
+ is_one_way = _is_path_one_way(path, bidirectional, oneway_values)
843
+ if is_one_way and _is_path_reversed(path, reversed_values):
844
+ nodes.reverse()
845
+
846
+ # set the oneway attribute, but only if when not forcing all edges to
847
+ # oneway with the all_oneway setting. With the all_oneway setting, you
848
+ # want to preserve the original OSM oneway attribute for later clarity
849
+ if not settings.all_oneway:
850
+ path["oneway"] = is_one_way
851
+
852
+ # zip path nodes to get (u, v) tuples like [(0,1), (1,2), (2,3)].
853
+ edges = list(pairwise(nodes))
854
+
855
+ # add all the edge tuples and give them the path's tag:value attrs
856
+ path["reversed"] = False
857
+ G.add_edges_from(edges, **path)
858
+
859
+ # if the path is NOT one-way, reverse direction of each edge and add
860
+ # this path going the opposite direction too
861
+ if not is_one_way:
862
+ path["reversed"] = True
863
+ G.add_edges_from([(v, u) for u, v in edges], **path)