openstreetmap-tools/web/templates/index.html
Edward Betts a79b0a7098 Make public transport routes discoverable with shareable search
Add one-click examples and route search by number or metadata, with transport type, location and map-area filters. Preserve search filters and bounds in shareable URLs and restore searches on reload and browser navigation.

Validate with 91 passing tests, including desktop and mobile browser coverage, and strict mypy checks.

Closes #2
2026-09-30 22:26:52 +01:00

230 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>{% if route_name %}{{ route_name }} – {% endif %}OSM Public Transport → GeoJSON</title>
{% if route_name %}<meta property="og:title" content="{{ route_name }} – OSM Public Transport → GeoJSON">{% endif %}
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<nav class="navbar navbar-dark bg-dark px-3" style="height:56px">
<a class="navbar-brand" href="{{ url_for('index') }}">OSM Public Transport → GeoJSON</a>
<a class="nav-link text-white" href="{{ url_for('docs') }}">API docs</a>
</nav>
<div class="container-fluid h-100 p-0">
<div class="row g-0" id="main-row">
<!-- Sidebar -->
<div class="col-3 border-end" id="sidebar">
<!-- Mobile bottom-sheet handle (hidden on desktop) -->
<div id="sidebar-handle" class="d-flex d-md-none align-items-center gap-2 px-3">
<div id="sidebar-handle-pill"></div>
<span id="sidebar-handle-label" class="small text-muted">Controls &amp; stops</span>
</div>
<!-- Scrollable content area -->
<div id="sidebar-inner">
<form id="route-search-form" class="mb-3">
<label for="route-search" class="form-label fw-semibold small">Find a public transport route</label>
<div class="input-group input-group-sm mb-2">
<input id="route-search" type="search" class="form-control" required
minlength="2" maxlength="100" placeholder="e.g. M11 or Bristol Airport"
aria-describedby="search-help">
<button id="search-button" class="btn btn-primary" type="submit">Search</button>
</div>
<label for="search-mode" class="visually-hidden">Transport type</label>
<select id="search-mode" class="form-select form-select-sm">
<option value="">All transport types</option>
<option value="bus">Bus</option>
<option value="trolleybus">Trolleybus</option>
<option value="tram">Tram</option>
<option value="subway">Subway / metro</option>
<option value="train">Train</option>
<option value="light_rail">Light rail</option>
<option value="monorail">Monorail</option>
<option value="ferry">Ferry</option>
<option value="funicular">Funicular</option>
</select>
<label for="search-location" class="form-label small mt-2 mb-1">Location (optional)</label>
<input id="search-location" class="form-control form-control-sm" type="text"
minlength="2" maxlength="100" placeholder="e.g. Bristol, UK" autocomplete="off"
aria-describedby="search-area-help">
<div class="form-check mt-2">
<input id="search-map-area" class="form-check-input" type="checkbox">
<label for="search-map-area" class="form-check-label small">Search this map area</label>
</div>
<p id="search-help" class="form-text mb-0">Search by route number, name,
destination, operator or network. Route numbers match exactly.</p>
<p id="search-area-help" class="form-text mb-0">Leave location blank to search worldwide,
or limit to a place or the current map view. Routes can extend outside the area.</p>
<p id="search-area-label" class="small mt-2 mb-0"></p>
<p class="form-text mb-0">After searching, copy the page URL to share these filters.</p>
<div id="location-results" class="list-group small mt-2"></div>
<p id="search-status" class="small mt-2 mb-0" role="status" aria-live="polite"></p>
<div id="search-results" class="list-group small mt-2"></div>
</form>
<!-- A first route is one click away; no OSM knowledge required. -->
<details class="mb-3"{% if not relation_id %} open{% endif %}>
<summary class="fw-semibold">Try a public transport route</summary>
<p class="small mt-2 mb-2">View a route on the map and download it as GeoJSON.
Choose an example to get started:</p>
<div class="list-group small mb-2">
<a class="list-group-item list-group-item-action"
href="{{ url_for('route_page', relation_id=18892969) }}">
<span class="fw-semibold d-block">Bristol airport bus A1</span>
<span class="text-muted">Airport → Bus Station · Bus</span>
</a>
<a class="list-group-item list-group-item-action"
href="{{ url_for('route_page', relation_id=15083963) }}">
<span class="fw-semibold d-block">Istanbul Metro M11</span>
<span class="text-muted">One direction · Subway</span>
</a>
<a class="list-group-item list-group-item-action"
href="{{ url_for('route_page', relation_id=15083966) }}">
<span class="fw-semibold d-block">Istanbul M11 — all directions</span>
<span class="text-muted">Compare routes, then choose a direction</span>
</a>
</div>
<details{% if not relation_id %} open{% endif %} class="small">
<summary class="fw-semibold">How to use this tool</summary>
<ol class="ps-3 mt-2 mb-0">
<li>Search and choose a result, try an example, or paste a route URL below.</li>
<li>Download the full route, or select a start stop and then an end stop to preview a segment.</li>
<li>Choose whether to include stops, then download your GeoJSON file.</li>
</ol>
</details>
</details>
<!-- Load form -->
<form method="post" action="{{ url_for('load') }}" class="mb-3">
<label for="relation-input" class="form-label fw-semibold small">Load your own route</label>
<div class="input-group input-group-sm">
<input type="text" name="relation" id="relation-input" class="form-control"
placeholder="Relation ID or OSM URL" aria-describedby="relation-help" required
value="{{ relation_id or '' }}">
<button class="btn btn-primary" type="submit">Load</button>
</div>
<p id="relation-help" class="form-text mb-0">Paste an OpenStreetMap route relation URL
(e.g. https://www.openstreetmap.org/relation/15083963) or its numeric ID.
A relation groups the ways and stops belonging to a route.</p>
</form>
<!-- Error alert -->
{% if error %}
<div class="alert alert-danger alert-dismissible py-2 small" role="alert">
{{ error }}
<button type="button" class="btn-close btn-sm" data-bs-dismiss="alert"></button>
</div>
{% endif %}
<div id="js-alert" class="alert alert-danger alert-dismissible py-2 small d-none" role="alert">
<span id="js-alert-msg"></span>
<button type="button" class="btn-close btn-sm" data-bs-dismiss="alert"></button>
</div>
<!-- Route master panel (shown when a route_master relation is loaded) -->
<div id="route-master-panel" class="d-none">
<div class="mb-3">
<div class="fw-semibold" id="route-master-name"></div>
<a id="route-master-osm-link" class="small text-muted" target="_blank" rel="noopener">View on OSM ↗</a>
</div>
<div class="fw-semibold small mb-1">Routes</div>
<div id="route-master-list"></div>
</div>
<!-- Route info (hidden until loaded) -->
<div id="route-panel" class="d-none">
<div class="mb-3">
<div class="fw-semibold" id="route-name"></div>
<a id="route-osm-link" class="small text-muted" target="_blank" rel="noopener">View on OSM ↗</a>
</div>
<!-- From / To slots -->
<div class="mb-1">
<label class="form-label fw-semibold small mb-1">From</label>
<div class="slot-box active" id="slot-from" title="Click to make active, then click a stop">
<span class="placeholder" id="slot-from-text">Click a stop…</span>
<span class="pencil" id="slot-from-pencil">✎</span>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold small mb-1">To</label>
<div class="slot-box" id="slot-to" title="Click to make active, then click a stop">
<span class="placeholder" id="slot-to-text">Click a stop…</span>
<span class="pencil d-none" id="slot-to-pencil">✎</span>
</div>
</div>
<!-- Clear button -->
<button class="btn btn-outline-secondary btn-sm w-100 mb-3" id="btn-clear" type="button">
✕ Clear selection
</button>
<!-- Include stops toggle -->
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="include-stops" checked>
<label class="form-check-label small" for="include-stops">Include stops in GeoJSON</label>
</div>
<!-- Download buttons -->
<div class="d-grid gap-2 mb-3">
<button class="btn btn-outline-secondary btn-sm disabled" id="btn-download-segment">
↓ Download segment
</button>
<button class="btn btn-outline-secondary btn-sm" id="btn-download-full">
↓ Download full route
</button>
</div>
<!-- Other directions -->
<div id="other-directions-panel" class="d-none mb-3">
<div class="fw-semibold small mb-1">Other directions</div>
<div id="other-directions-list"></div>
</div>
<!-- Stop list -->
<div class="fw-semibold small mb-1">Stops <span id="stop-count" class="text-muted"></span></div>
<div id="stop-list"></div>
</div>
</div><!-- /sidebar-inner -->
</div><!-- /sidebar -->
<!-- Map -->
<div class="col-9" id="map">
<!-- Mobile floating button to open the panel (hidden on desktop) -->
<button id="panel-fab" class="d-md-none" type="button" aria-label="Open controls">
<span id="panel-fab-icon">☰</span>
<span id="panel-fab-label">Controls</span>
</button>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
// Injected by Flask so app.js works correctly under any mount path.
const RELATION_ID = {{ relation_id | tojson }};
const URLS = {
indexPage: {{ url_for('index') | tojson }},
searchApi: {{ url_for('api_search') | tojson }},
locationsApi: {{ url_for('api_locations') | tojson }},
routeApi: {{ url_for('api_route', relation_id=0)[:-1] | tojson }},
segmentApi: {{ url_for('api_segment', relation_id=0)[:-1] | tojson }},
routeMasterApi: {{ url_for('api_route_master', relation_id=0)[:-1] | tojson }},
routePage: {{ url_for('route_page', relation_id=0)[:-1] | tojson }},
};
</script>
<script src="{{ url_for('static', filename='app.js') }}"></script>
</body>
</html>