Spaces:
Sleeping
Sleeping
Upload 79 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- Dockerfile +18 -0
- README.md +26 -4
- app.py +45 -0
- osmnx/mcp_output/README_MCP.md +70 -0
- osmnx/mcp_output/analysis.json +251 -0
- osmnx/mcp_output/diff_report.md +63 -0
- osmnx/mcp_output/mcp_plugin/__init__.py +0 -0
- osmnx/mcp_output/mcp_plugin/adapter.py +242 -0
- osmnx/mcp_output/mcp_plugin/main.py +13 -0
- osmnx/mcp_output/mcp_plugin/mcp_service.py +79 -0
- osmnx/mcp_output/requirements.txt +11 -0
- osmnx/mcp_output/start_mcp.py +30 -0
- osmnx/mcp_output/workflow_summary.json +194 -0
- osmnx/source/.pre-commit-config.yaml +53 -0
- osmnx/source/CHANGELOG.md +690 -0
- osmnx/source/CITATION.cff +32 -0
- osmnx/source/CONTRIBUTING.md +39 -0
- osmnx/source/LICENSE.txt +21 -0
- osmnx/source/README.md +33 -0
- osmnx/source/__init__.py +4 -0
- osmnx/source/docs/.readthedocs.yaml +16 -0
- osmnx/source/docs/Makefile +20 -0
- osmnx/source/docs/make.bat +35 -0
- osmnx/source/docs/requirements-docs.txt +4 -0
- osmnx/source/docs/source/conf.py +64 -0
- osmnx/source/docs/source/further-reading.rst +40 -0
- osmnx/source/docs/source/getting-started.rst +184 -0
- osmnx/source/docs/source/index.rst +84 -0
- osmnx/source/docs/source/installation.rst +37 -0
- osmnx/source/docs/source/internals-reference.rst +196 -0
- osmnx/source/docs/source/user-reference.rst +142 -0
- osmnx/source/environments/create_conda_env.sh +23 -0
- osmnx/source/environments/docker/Dockerfile +24 -0
- osmnx/source/environments/docker/build_image.sh +7 -0
- osmnx/source/environments/docker/install.sh +21 -0
- osmnx/source/osmnx/__init__.py +37 -0
- osmnx/source/osmnx/_api_v1.py +53 -0
- osmnx/source/osmnx/_errors.py +21 -0
- osmnx/source/osmnx/_http.py +332 -0
- osmnx/source/osmnx/_nominatim.py +151 -0
- osmnx/source/osmnx/_osm_xml.py +439 -0
- osmnx/source/osmnx/_overpass.py +493 -0
- osmnx/source/osmnx/_validate.py +387 -0
- osmnx/source/osmnx/bearing.py +302 -0
- osmnx/source/osmnx/convert.py +564 -0
- osmnx/source/osmnx/distance.py +545 -0
- osmnx/source/osmnx/elevation.py +331 -0
- osmnx/source/osmnx/features.py +734 -0
- osmnx/source/osmnx/geocoder.py +244 -0
- 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:
|
| 5 |
colorTo: purple
|
| 6 |
sdk: docker
|
|
|
|
|
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
[](https://pypi.org/project/osmnx/)
|
| 4 |
+
[](https://pepy.tech/project/osmnx)
|
| 5 |
+
[](https://osmnx.readthedocs.io/)
|
| 6 |
+
[](https://github.com/gboeing/osmnx/actions/workflows/ci.yml)
|
| 7 |
+
[](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)
|