diff --git a/AGENTS.md b/AGENTS.md index 950e309..11dbc37 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,8 +4,9 @@ Guidelines for AI coding agents working in this repository. ## Project overview -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. +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. ## Repository layout @@ -14,20 +15,10 @@ 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 @@ -44,13 +35,10 @@ 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 (CLI) +- Errors go to stderr; data output goes to stdout - 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 @@ -63,30 +51,6 @@ 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: @@ -110,9 +74,7 @@ API. Fixture data is stored in `tests/fixtures/` as saved API responses. | ID | Description | |----------|------------------------------------------| -| 15083963 | M11 Istanbul Metro (subway, direction 1) | -| 15083964 | M11 Istanbul Metro (subway, direction 2) | -| 15083966 | M11 Istanbul Metro (route_master) | +| 15083963 | M11 Istanbul Metro (subway) | | 18892969 | Bus A1: Bristol Airport → Bus Station | ## Dependencies diff --git a/README.md b/README.md index 9ee0234..063030f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # openstreetmap-tools -A collection of tools for working with OpenStreetMap public transport data. +A collection of command-line tools for working with OpenStreetMap data. ## Tools @@ -9,7 +9,7 @@ A collection of tools for working with OpenStreetMap public transport data. Fetch an OSM public transport route relation and list its stops, or export the route as GeoJSON. -#### CLI usage +#### Usage ``` osm-pt-geojson list-stations @@ -67,46 +67,6 @@ 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/homepage/index.html b/homepage/index.html deleted file mode 100644 index c4866ef..0000000 --- a/homepage/index.html +++ /dev/null @@ -1,392 +0,0 @@ - - - - - - OpenStreetMap Tools - - - - - - - - -
-
-

openstreetmap.tools

-

OpenStreetMap
Tools

-

A collection of tools for working with OpenStreetMap data.

-
-
- -
- - -
- - - -
-
-

OWL Places

- Wikidata -
-

- Link OpenStreetMap places to Wikidata entries, one place at a time. - Search for an OSM object, find its Wikidata counterpart, and add the - connection — improving data quality across both datasets. -

- Open OWL Places → -
- -
-
-

OWL Map

- Wikidata -
-

- Map-based OSM/Wikidata linker. Pan to any area, see which objects - need Wikidata links, and connect them at speed — faster to get - started than OWL Places, built for covering ground at scale. -

- Open OWL Map → -
- -
-
- - - - - - - diff --git a/web/app.py b/web/app.py index d27babd..65da89e 100644 --- a/web/app.py +++ b/web/app.py @@ -80,12 +80,6 @@ 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/static/app.js b/web/static/app.js index 8bb7913..a94b6b5 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -22,7 +22,6 @@ let routeMasterLayers = []; // coloured polylines when viewing a route_master // ── State ───────────────────────────────────────────────────────────────── -let currentRelationId = RELATION_ID; // tracks the currently loaded relation let routeData = null; // response from /api/route/ let segmentGeoJson = null; // last response from /api/segment/ (always includes stops) let activeSlot = 'from'; // 'from' | 'to' | null @@ -302,21 +301,6 @@ document.getElementById('include-stops').addEventListener('change', () => { document.getElementById('slot-from').addEventListener('click', () => setActiveSlot('from')); document.getElementById('slot-to').addEventListener('click', () => setActiveSlot('to')); -// ── Navigation ───────────────────────────────────────────────────────────── - -/** - * Load a relation without a full page reload, updating the browser URL. - * Allows middle-click / ctrl-click to still open in a new tab via the href. - * @param {number} id - * @param {MouseEvent} e - */ -function navigateTo(id, e) { - if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return; - e.preventDefault(); - history.pushState(null, '', `/${id}`); - loadRoute(id); -} - // ── API calls ────────────────────────────────────────────────────────────── /** @@ -336,7 +320,6 @@ async function loadRoute(relationId) { return; } routeData = data; - currentRelationId = relationId; // Clean up any previous route_master view for (const l of routeMasterLayers) l.remove(); routeMasterLayers = []; @@ -349,8 +332,6 @@ async function loadRoute(relationId) { // Show route panel document.getElementById('route-panel').classList.remove('d-none'); document.getElementById('route-name').textContent = data.name; - document.getElementById('route-osm-link').href = - `https://www.openstreetmap.org/relation/${relationId}`; // Draw full route in grey if (fullRouteLayer) fullRouteLayer.remove(); @@ -377,7 +358,6 @@ async function loadRoute(relationId) { a.href = `/${dir.id}`; a.className = 'stop-item d-block text-decoration-none'; a.textContent = dir.name; - a.addEventListener('click', (e) => navigateTo(dir.id, e)); dirList.appendChild(a); } } else { @@ -393,7 +373,7 @@ async function loadRoute(relationId) { */ async function loadSegment() { if (!selectedFrom || !selectedTo || !routeData) return; - const rid = currentRelationId; + const rid = RELATION_ID; const params = new URLSearchParams({ from: selectedFrom, to: selectedTo, stops: '1' }); try { const resp = await fetch(`/api/segment/${rid}?${params}`); @@ -437,8 +417,6 @@ async function loadRouteMaster(relationId) { document.getElementById('route-panel').classList.add('d-none'); document.getElementById('route-master-panel').classList.remove('d-none'); document.getElementById('route-master-name').textContent = data.name; - document.getElementById('route-master-osm-link').href = - `https://www.openstreetmap.org/relation/${relationId}`; const colours = ['#0d6efd', '#dc3545', '#198754', '#fd7e14', '#6f42c1']; const list = document.getElementById('route-master-list'); @@ -466,7 +444,6 @@ async function loadRouteMaster(relationId) { a.href = `/${route.id}`; a.className = 'text-decoration-none text-reset flex-grow-1'; a.textContent = route.name; - a.addEventListener('click', (e) => navigateTo(route.id, e)); div.appendChild(dot); div.appendChild(a); @@ -485,11 +462,6 @@ async function loadRouteMaster(relationId) { // ── Init ─────────────────────────────────────────────────────────────────── -window.addEventListener('popstate', () => { - const match = location.pathname.match(/^\/(\d+)$/); - if (match) loadRoute(parseInt(match[1], 10)); -}); - if (RELATION_ID) { loadRoute(RELATION_ID); } diff --git a/web/templates/api.html b/web/templates/api.html deleted file mode 100644 index 74bf442..0000000 --- a/web/templates/api.html +++ /dev/null @@ -1,269 +0,0 @@ - - - - - - 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 a1f86f6..ed496f0 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -13,7 +13,6 @@
@@ -49,7 +48,6 @@
Routes
@@ -59,7 +57,6 @@