diff --git a/AGENTS.md b/AGENTS.md index 11dbc37..950e309 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,9 +4,8 @@ Guidelines for AI coding agents working in this repository. ## Project overview -A collection of Python CLI tools for working with OpenStreetMap data. Each -tool is a standalone Python script using Click for the CLI interface and -Requests for HTTP calls. +A collection of Python tools for working with OpenStreetMap public transport +data. Includes a CLI tool and a Flask web frontend with a JSON API. ## Repository layout @@ -15,10 +14,20 @@ pyproject.toml - Package metadata and build configuration src/ osm_geojson/ __init__.py - Top-level package marker + py.typed - Marker for mypy typed package pt/ __init__.py - Public transport subpackage core.py - Data fetching and processing functions cli.py - Click CLI commands +web/ + app.py - Flask application (routes + API endpoints) + templates/ + index.html - Main map UI + api.html - API documentation page (served at /docs) + static/ + app.js - Frontend JavaScript + style.css - CSS for the map UI + favicon.svg - SVG favicon tests/ fixtures/ - Saved OSM API responses for offline testing test_osm_pt_geojson.py - Test suite @@ -35,10 +44,13 @@ point registered in `pyproject.toml` under `[project.scripts]`. - Python 3.11+ - CLI via [Click](https://click.palletsprojects.com/) - HTTP via [Requests](https://requests.readthedocs.io/) +- Web frontend via [Flask](https://flask.palletsprojects.com/) 3.x - Parse XML with lxml if needed; prefer the OSM JSON API where possible -- Errors go to stderr; data output goes to stdout +- Errors go to stderr; data output goes to stdout (CLI) - GeoJSON output uses `ensure_ascii=False` - All modules, functions, and test functions must have docstrings +- Library code (`core.py`) raises `OsmError` instead of calling `sys.exit()`; + the CLI and Flask views catch `OsmError` and handle it appropriately ## OSM API @@ -51,6 +63,30 @@ https://www.openstreetmap.org/api/0.6/ No authentication is required for read-only access. Include a descriptive `User-Agent` header in all requests. +## Web frontend API endpoints + +| Endpoint | Description | +|---|---| +| `GET /api/route/` | Full route GeoJSON, stop list, and sibling routes | +| `GET /api/segment/?from=NAME&to=NAME[&stops=0]` | Segment between two named stops | +| `GET /api/route_master/` | All member routes of a route_master | +| `GET /docs` | API documentation page | + +All API responses are JSON. Errors return `{"error": "", "message": ""}`. + +## Running the web frontend + +``` +cd web +flask --app app.py run +``` + +The web dependencies (Flask) are not in `pyproject.toml`; install separately: + +``` +pip install flask +``` + ## Type checking All code uses type hints. Run mypy in strict mode to check: @@ -74,7 +110,9 @@ API. Fixture data is stored in `tests/fixtures/` as saved API responses. | ID | Description | |----------|------------------------------------------| -| 15083963 | M11 Istanbul Metro (subway) | +| 15083963 | M11 Istanbul Metro (subway, direction 1) | +| 15083964 | M11 Istanbul Metro (subway, direction 2) | +| 15083966 | M11 Istanbul Metro (route_master) | | 18892969 | Bus A1: Bristol Airport → Bus Station | ## Dependencies diff --git a/README.md b/README.md index 063030f..9ee0234 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # openstreetmap-tools -A collection of command-line tools for working with OpenStreetMap data. +A collection of tools for working with OpenStreetMap public transport data. ## Tools @@ -9,7 +9,7 @@ A collection of command-line tools for working with OpenStreetMap data. Fetch an OSM public transport route relation and list its stops, or export the route as GeoJSON. -#### Usage +#### CLI usage ``` osm-pt-geojson list-stations @@ -67,6 +67,46 @@ cd openstreetmap-tools pip install -e . ``` +--- + +### Web frontend + +An interactive map interface for browsing and downloading public transport +routes as GeoJSON. + +#### Features + +- Enter a relation ID or OSM URL to load a route onto the map. +- Click stops in the sidebar list or on the map to set start and end points. +- Preview the selected segment highlighted on the map. +- Toggle stop points in/out of the GeoJSON before downloading. +- Download a segment or the full route as a `.geojson` file. +- Entering a `route_master` ID shows all directions on the map with links to + each individual route. +- Bookmarkable URLs: `/` loads that route directly. + +#### Running the dev server + +``` +cd web +flask --app app.py run +``` + +Open `http://127.0.0.1:5000`. + +#### API + +The web frontend exposes a JSON API. Full documentation is available at `/docs` +when the server is running. + +| Endpoint | Description | +|---|---| +| `GET /api/route/` | Full route GeoJSON, stop list, and sibling routes | +| `GET /api/segment/?from=NAME&to=NAME` | Segment between two named stops | +| `GET /api/route_master/` | All member routes of a route_master | + +--- + ## Licence MIT License. Copyright (c) 2026 Edward Betts. diff --git a/web/app.py b/web/app.py index 65da89e..d27babd 100644 --- a/web/app.py +++ b/web/app.py @@ -80,6 +80,12 @@ def check_route_tags( return None +@app.route("/docs") +def docs() -> ResponseReturnValue: + """Render the API documentation page.""" + return render_template("api.html") + + @app.route("/") def index() -> ResponseReturnValue: """Render the landing page with no relation loaded.""" diff --git a/web/templates/api.html b/web/templates/api.html new file mode 100644 index 0000000..74bf442 --- /dev/null +++ b/web/templates/api.html @@ -0,0 +1,269 @@ + + + + + + API Documentation – OSM Public Transport → GeoJSON + + + + + + + + +
+ +

API Documentation

+

JSON API for fetching OSM public transport routes as GeoJSON.

+ +

All endpoints are read-only and require no authentication. Data is fetched + live from the OSM API v0.6. + Relation IDs can be found on + openstreetmap.org — search for a + route and click through to its relation page.

+ +

All error responses return JSON with at least error (machine-readable + code) and message (human-readable description) fields.

+ + +

+ GET + /api/route/<relation_id> +

+

Returns the full route as a GeoJSON FeatureCollection, the + ordered list of stops, and links to sibling routes in the same + route_master (if any).

+ +

Path parameters

+ + + + + +
ParameterTypeDescription
relation_idintegerOSM relation ID of a public transport route.
+ +

Success response 200 OK

+
{
+  "name": "M11: Arnavutköy Hastane → Gayrettepe",
+  "ref": "M11",
+  "stops": [
+    { "name": "Arnavutköy Hastane", "lat": 41.1234, "lon": 28.7654 },
+    { "name": "İstanbul Havalimanı", "lat": 41.2701, "lon": 28.7519 },
+    ...
+  ],
+  "geojson": {
+    "type": "FeatureCollection",
+    "features": [
+      {
+        "type": "Feature",
+        "geometry": { "type": "LineString", "coordinates": [[28.765, 41.123], ...] },
+        "properties": { "name": "M11: Arnavutköy Hastane → Gayrettepe",
+                        "ref": "M11", "from": "Arnavutköy Hastane",
+                        "to": "Gayrettepe", "route": "subway" }
+      },
+      {
+        "type": "Feature",
+        "geometry": { "type": "Point", "coordinates": [28.765, 41.123] },
+        "properties": { "name": "Arnavutköy Hastane" }
+      },
+      ...
+    ]
+  },
+  "other_directions": [
+    { "id": 15083964, "name": "M11: Gayrettepe → Arnavutköy Hastane", "ref": "M11" }
+  ]
+}
+ +

The geojson field is a FeatureCollection containing + one LineString for the route geometry followed by one + Point per stop. The LineString carries the route tags + as properties (name, ref, from, + to, route). Each Point has a + name property.

+ +

other_directions lists sibling routes from the same + route_master relation. It is an empty array if the route has no + parent route_master.

+ +

Error responses

+ + + + + + + + +
StatuserrorWhen
404osm_errorRelation not found on OpenStreetMap.
422is_route_masterRelation is a route_master. Use /api/route_master/<id> instead.
422not_public_transportRelation exists but is not a supported public transport route.
502osm_errorOSM API returned an unexpected error.
+ +

Example

+
GET /api/route/15083963
+ + +

+ GET + /api/segment/<relation_id> +

+

Returns a GeoJSON FeatureCollection for the portion of the + route between two named stops. Stops are matched case-insensitively.

+ +

Path parameters

+ + + + + +
ParameterTypeDescription
relation_idintegerOSM relation ID of a public transport route.
+ +

Query parameters

+ + + + + + + +
ParameterRequiredDescription
fromYesName of the start stop (case-insensitive).
toYesName of the end stop (case-insensitive).
stopsNoInclude stop Point features. 1 (default) to include, 0 to omit.
+ +

Success response 200 OK

+
{
+  "type": "FeatureCollection",
+  "features": [
+    {
+      "type": "Feature",
+      "geometry": { "type": "LineString", "coordinates": [[28.800, 41.259], ...] },
+      "properties": { "name": "M11: Arnavutköy Hastane → Gayrettepe",
+                      "ref": "M11", "from": "Arnavutköy Hastane",
+                      "to": "Gayrettepe", "route": "subway" }
+    },
+    {
+      "type": "Feature",
+      "geometry": { "type": "Point", "coordinates": [28.800, 41.259] },
+      "properties": { "name": "İstanbul Havalimanı" }
+    },
+    {
+      "type": "Feature",
+      "geometry": { "type": "Point", "coordinates": [28.820, 41.247] },
+      "properties": { "name": "Hasdal" }
+    }
+  ]
+}
+ +

The LineString is trimmed to the shortest path between the + two stops along the route geometry. The from stop is treated as + the start regardless of the order the names are supplied — the segment always + follows the route direction.

+ +

Error responses

+ + + + + + + + + +
StatuserrorWhen
400missing_paramsfrom or to parameter is absent.
404osm_errorRelation not found on OpenStreetMap.
404station_not_foundOne or both stop names were not found. The response includes an available array listing valid stop names.
422not_public_transportRelation is not a supported public transport route.
502osm_errorOSM API returned an unexpected error.
+ +

Example

+
GET /api/segment/15083963?from=İstanbul+Havalimanı&to=Hasdal
+
GET /api/segment/15083963?from=İstanbul+Havalimanı&to=Hasdal&stops=0
+ + +

+ GET + /api/route_master/<relation_id> +

+

Returns all member routes of an OSM route_master relation, + each with its GeoJSON geometry (stops omitted). Use this to display all + directions of a line on a map.

+ +

Path parameters

+ + + + + +
ParameterTypeDescription
relation_idintegerOSM relation ID of a route_master relation.
+ +

Success response 200 OK

+
{
+  "name": "M11",
+  "ref": "M11",
+  "routes": [
+    {
+      "id": 15083963,
+      "name": "M11: Arnavutköy Hastane → Gayrettepe",
+      "ref": "M11",
+      "from": "Arnavutköy Hastane",
+      "to": "Gayrettepe",
+      "geojson": {
+        "type": "FeatureCollection",
+        "features": [
+          {
+            "type": "Feature",
+            "geometry": { "type": "LineString", "coordinates": [[28.765, 41.123], ...] },
+            "properties": { "name": "M11: Arnavutköy Hastane → Gayrettepe", ... }
+          }
+        ]
+      }
+    },
+    {
+      "id": 15083964,
+      "name": "M11: Gayrettepe → Arnavutköy Hastane",
+      "ref": "M11",
+      "from": "Gayrettepe",
+      "to": "Arnavutköy Hastane",
+      "geojson": { ... }
+    }
+  ]
+}
+ +

Each entry in routes contains the individual route's + id, name, ref, from, + to tags, and a geojson FeatureCollection + with only the LineString geometry (no stop points). If a member + route cannot be fetched, it is included with "geojson": null.

+ +

Error responses

+ + + + + + + +
StatuserrorWhen
404osm_errorRelation not found on OpenStreetMap.
422osm_errorRelation exists but is not a route_master.
502osm_errorOSM API returned an unexpected error.
+ +

Example

+
GET /api/route_master/15083966
+ + +

Supported route types

+

The following OSM route tag values are accepted:

+

+ bus   + ferry   + funicular   + light_rail   + monorail   + subway   + train   + tram   + trolleybus +

+ +
+ + diff --git a/web/templates/index.html b/web/templates/index.html index ed496f0..4eb6932 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -13,6 +13,7 @@