330 lines
16 KiB
HTML
330 lines
16 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>Mobility Workbench</title>
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin="" />
|
||
<link rel="stylesheet" href="/static/style.css?v=20260701-harmonizer-module" />
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<div>
|
||
<h1>Mobility Workbench</h1>
|
||
<p>Harmonized transit, mapping data, route layer, map review, and journey tests.</p>
|
||
</div>
|
||
<div class="actions">
|
||
<button id="refreshBtn">Refresh</button>
|
||
</div>
|
||
</header>
|
||
|
||
<main>
|
||
<aside>
|
||
<div class="sidebar-content">
|
||
<details class="card sidebar-section" data-sidebar-section="stats" open>
|
||
<summary><h2>Stats</h2></summary>
|
||
<div class="sidebar-section-body">
|
||
<div id="stats" class="stats"></div>
|
||
</div>
|
||
</details>
|
||
|
||
<details class="card sidebar-section" data-sidebar-section="qa" open>
|
||
<summary><h2>QA</h2></summary>
|
||
<div class="sidebar-section-body">
|
||
<div class="qa-toolbar">
|
||
<button type="button" id="refreshQaBtn">Refresh QA</button>
|
||
</div>
|
||
<div id="qaDashboard" class="qa-dashboard muted">No QA loaded.</div>
|
||
</div>
|
||
</details>
|
||
|
||
<details class="card sidebar-section" data-sidebar-section="jobs" open>
|
||
<summary><h2>Jobs</h2></summary>
|
||
<div class="sidebar-section-body">
|
||
<div id="jobs" class="jobs muted">No jobs loaded.</div>
|
||
</div>
|
||
</details>
|
||
|
||
<details class="card sidebar-section" data-sidebar-section="harmonization" open>
|
||
<summary><h2>GTFS Harmonization</h2></summary>
|
||
<div class="sidebar-section-body">
|
||
<details class="nested-section" data-sidebar-section="add-gtfs-source">
|
||
<summary><h3>Add GTFS source</h3></summary>
|
||
<div class="nested-section-body">
|
||
<form id="sourceForm">
|
||
<input name="catalog_entry_id" type="hidden" />
|
||
<input name="kind" type="hidden" value="gtfs" />
|
||
<label>Name <input name="name" required placeholder="DELFI / national GTFS" /></label>
|
||
<label>URL or path <input name="url" required placeholder="https://.../feed.zip or ./data/feed.zip" /></label>
|
||
<label>Country <input name="country" placeholder="DE" maxlength="8" /></label>
|
||
<label>License <input name="license" placeholder="ODbL / CC-BY / unknown" /></label>
|
||
<button type="submit">Add GTFS source</button>
|
||
</form>
|
||
</div>
|
||
</details>
|
||
|
||
<details class="nested-section source-catalog-card" data-sidebar-section="source-catalog">
|
||
<summary><h3>Transit source catalog</h3></summary>
|
||
<div class="nested-section-body">
|
||
<div id="sourceCatalogSummary" class="muted"></div>
|
||
<div class="filter-row source-catalog-filter">
|
||
<input id="sourceCatalogSearch" placeholder="Search catalog" />
|
||
<input id="sourceCatalogCountry" placeholder="Country" />
|
||
<select id="sourceCatalogPriority">
|
||
<option value="">all priorities</option>
|
||
<option value="P0">P0</option>
|
||
<option value="P0 fallback">P0 fallback</option>
|
||
<option value="P1">P1</option>
|
||
<option value="P2">P2</option>
|
||
<option value="P3">P3</option>
|
||
<option value="P4">P4</option>
|
||
<option value="P5">P5</option>
|
||
</select>
|
||
</div>
|
||
<div class="source-catalog-actions">
|
||
<button type="button" id="importSourceCatalogBtn">Import catalog</button>
|
||
<button type="button" id="importIngestableSourcesBtn">Import ingestable seeds</button>
|
||
</div>
|
||
<div id="sourceCatalog"></div>
|
||
</div>
|
||
</details>
|
||
|
||
<details class="nested-section" data-sidebar-section="gtfs-feed-qa" open>
|
||
<summary><h3>Feed QA</h3></summary>
|
||
<div class="nested-section-body">
|
||
<div class="qa-toolbar">
|
||
<button type="button" id="refreshGtfsHarmonizationBtn">Refresh feeds</button>
|
||
</div>
|
||
<div id="gtfsHarmonizationInventory" class="harmonization-inventory muted">No GTFS feed QA loaded.</div>
|
||
</div>
|
||
</details>
|
||
|
||
<details class="nested-section" data-sidebar-section="gtfs-source-management" open>
|
||
<summary><h3>GTFS source library</h3></summary>
|
||
<div class="nested-section-body">
|
||
<div class="filter-row">
|
||
<input id="sourceSearch" placeholder="Filter GTFS sources" />
|
||
</div>
|
||
<div id="sources"></div>
|
||
</div>
|
||
</details>
|
||
</div>
|
||
</details>
|
||
|
||
<details class="card sidebar-section" data-sidebar-section="mapping" open>
|
||
<summary><h2>Mapping Data</h2></summary>
|
||
<div class="sidebar-section-body">
|
||
<details class="nested-section" data-sidebar-section="add-map-source">
|
||
<summary><h3>Add map source</h3></summary>
|
||
<div class="nested-section-body">
|
||
<form id="mappingSourceForm">
|
||
<input name="catalog_entry_id" type="hidden" />
|
||
<label>Name <input name="name" required placeholder="Germany OSM PBF" /></label>
|
||
<label>Kind
|
||
<select name="kind">
|
||
<option value="osm_pbf">OSM PBF extract</option>
|
||
<option value="osm_geojson">OSM transport GeoJSON</option>
|
||
<option value="osm_diff">OSM change diff</option>
|
||
</select>
|
||
</label>
|
||
<label>URL or path <input name="url" required placeholder="https://.../latest.osm.pbf or ./data/routes.geojson" /></label>
|
||
<label>Country <input name="country" placeholder="DE" maxlength="8" /></label>
|
||
<label>License <input name="license" placeholder="ODbL / CC-BY / unknown" /></label>
|
||
<button type="submit">Add map source</button>
|
||
</form>
|
||
</div>
|
||
</details>
|
||
|
||
<details class="nested-section source-catalog-card" data-sidebar-section="geofabrik">
|
||
<summary><h3>Geofabrik OSM</h3></summary>
|
||
<div class="nested-section-body">
|
||
<div class="filter-row geofabrik-filter">
|
||
<input id="geofabrikSearch" placeholder="Berlin, Germany, Hamburg" />
|
||
<button type="button" id="geofabrikSearchBtn">Search</button>
|
||
</div>
|
||
<label class="inline-check"><input id="geofabrikDiffSource" type="checkbox" checked /> add diff source metadata</label>
|
||
<div id="geofabrikResults" class="dataset-search-results muted">Search Geofabrik extracts, then add or import one as an OSM PBF source.</div>
|
||
</div>
|
||
</details>
|
||
|
||
<details class="nested-section" data-sidebar-section="mapping-source-management" open>
|
||
<summary><h3>Map source library</h3></summary>
|
||
<div class="nested-section-body">
|
||
<div class="filter-row">
|
||
<input id="mappingSourceSearch" placeholder="Filter map sources" />
|
||
<select id="mappingSourceKindFilter">
|
||
<option value="">all map kinds</option>
|
||
<option value="osm_geojson">OSM GeoJSON</option>
|
||
<option value="osm_pbf">OSM PBF</option>
|
||
<option value="osm_diff">OSM diff</option>
|
||
</select>
|
||
</div>
|
||
<div id="mappingSources"></div>
|
||
</div>
|
||
</details>
|
||
</div>
|
||
</details>
|
||
|
||
<details class="card sidebar-section" data-sidebar-section="datasets" open>
|
||
<summary><h2>Datasets</h2></summary>
|
||
<div class="sidebar-section-body">
|
||
<details class="nested-section" data-sidebar-section="dataset-pipeline" open>
|
||
<summary><h3>Derivation pipeline</h3></summary>
|
||
<div class="nested-section-body">
|
||
<div class="workflow-actions">
|
||
<button id="runMatchBtn" type="button">Run matcher</button>
|
||
<button id="buildRouteLayerBtn" type="button">Build route layer</button>
|
||
<button id="loadSampleBtn" type="button">Reset sample</button>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
|
||
<details class="nested-section" data-sidebar-section="dataset-search">
|
||
<summary><h3>Dataset search</h3></summary>
|
||
<div class="nested-section-body">
|
||
<form id="datasetSearchForm" class="dataset-search-form">
|
||
<input id="datasetSearchQuery" placeholder="Route, line, stop, shape ID" autocomplete="off" />
|
||
<div class="filter-row">
|
||
<label class="inline-check"><input id="datasetSearchActiveOnly" type="checkbox" checked /> active only</label>
|
||
<button type="submit">Search</button>
|
||
</div>
|
||
</form>
|
||
<div id="datasetSearchResults" class="dataset-search-results muted">Search all imported datasets by label, route ID, and route-layer reference.</div>
|
||
</div>
|
||
</details>
|
||
|
||
<details class="nested-section matches-card" data-sidebar-section="route-matches">
|
||
<summary><h3>Route matches</h3></summary>
|
||
<div class="nested-section-body">
|
||
<div class="filter-row">
|
||
<select id="matchStatusFilter">
|
||
<option value="">all</option>
|
||
<option value="matched">matched</option>
|
||
<option value="probable">probable</option>
|
||
<option value="weak">weak</option>
|
||
<option value="missing">missing</option>
|
||
<option value="accepted">accepted</option>
|
||
<option value="rejected">rejected</option>
|
||
</select>
|
||
<button id="reloadMatchesBtn">Reload</button>
|
||
</div>
|
||
<div id="matches"></div>
|
||
</div>
|
||
</details>
|
||
|
||
<details class="nested-section" data-sidebar-section="maintenance">
|
||
<summary><h3>Maintenance</h3></summary>
|
||
<div class="nested-section-body">
|
||
<div class="maintenance-grid">
|
||
<button type="button" data-admin-action="init-db">Init DB</button>
|
||
<button type="button" data-admin-action="backfill-gtfs-shapes">Backfill GTFS shapes</button>
|
||
<button type="button" data-admin-action="prune-cache-dry">Check cache</button>
|
||
<button type="button" data-admin-action="prune-cache">Prune cache</button>
|
||
<button type="button" data-admin-action="prune-inactive-dry">Check inactive</button>
|
||
<button type="button" data-admin-action="prune-inactive">Prune inactive</button>
|
||
<button type="button" data-admin-action="vacuum-db">Vacuum DB</button>
|
||
<button type="button" class="danger" data-admin-action="reset-db">Reset DB</button>
|
||
</div>
|
||
<div id="adminStatus" class="admin-status muted"></div>
|
||
</div>
|
||
</details>
|
||
</div>
|
||
</details>
|
||
|
||
<details class="card sidebar-section" data-sidebar-section="layers" open>
|
||
<summary><h2>Layers</h2></summary>
|
||
<div class="sidebar-section-body">
|
||
<div class="preset-row">
|
||
<button type="button" data-layer-preset="network">Network</button>
|
||
<button type="button" data-layer-preset="review">Matched/unmatched</button>
|
||
<button type="button" data-layer-preset="unmatched">Unmatched</button>
|
||
<button type="button" data-layer-preset="all">All</button>
|
||
</div>
|
||
<div id="layerControls" class="layer-controls"></div>
|
||
<div id="mapStatus" class="map-status muted"></div>
|
||
</div>
|
||
</details>
|
||
|
||
</div>
|
||
<button id="sidebarCollapseBtn" class="sidebar-collapse-handle" type="button" aria-label="Collapse left panel" title="Collapse left panel" aria-expanded="true">‹</button>
|
||
</aside>
|
||
|
||
<section class="map-panel">
|
||
<div id="map"></div>
|
||
<div id="mapLoading" class="map-loading" hidden>
|
||
<span class="spinner" aria-hidden="true"></span>
|
||
<span id="mapLoadingText">Loading map layers...</span>
|
||
</div>
|
||
<section class="map-floating journey-card">
|
||
<h2>Journey</h2>
|
||
<form id="journeyForm">
|
||
<div id="journeyTransitSnapshot" class="journey-snapshot muted">Transit snapshot loading...</div>
|
||
<label>From <input id="journeyFromQuery" placeholder="Hauptbahnhof" autocomplete="off" /></label>
|
||
<input id="journeyFromStop" type="hidden" />
|
||
<div id="journeyFromSuggestions" class="stop-suggestions"></div>
|
||
<button type="button" id="journeySwapBtn" class="journey-swap" title="Switch start and destination">Swap</button>
|
||
<label>To <input id="journeyToQuery" placeholder="Alexanderplatz" autocomplete="off" /></label>
|
||
<input id="journeyToStop" type="hidden" />
|
||
<div id="journeyToSuggestions" class="stop-suggestions"></div>
|
||
<label>Via <input id="journeyViaQuery" placeholder="optional stop" autocomplete="off" /></label>
|
||
<input id="journeyViaStop" type="hidden" />
|
||
<div id="journeyViaSuggestions" class="stop-suggestions"></div>
|
||
<div class="journey-mode" role="radiogroup" aria-label="Route mode">
|
||
<label><input type="radio" name="journeyMode" value="transit" checked /> Public transport</label>
|
||
<label><input type="radio" name="journeyMode" value="walk" /> Walk</label>
|
||
<label><input type="radio" name="journeyMode" value="drive" /> Car</label>
|
||
</div>
|
||
<div class="journey-options">
|
||
<label>Date <input id="journeyServiceDate" type="date" /></label>
|
||
<label>Departure <input id="journeyDeparture" type="time" value="08:00" /></label>
|
||
<label>Transfer buffer <input id="journeyTransferMinutes" type="number" min="0" max="60" step="1" value="2" /></label>
|
||
<label>Rank by
|
||
<select id="journeyRanking">
|
||
<option value="recommended">Recommended</option>
|
||
<option value="earliest_arrival">Earliest arrival</option>
|
||
<option value="duration">Shortest duration</option>
|
||
<option value="fewest_transfers">Fewest transfers</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
<label class="journey-direct"><input id="journeyDirectOnly" type="checkbox" /> Direct public transport only</label>
|
||
<div class="journey-actions">
|
||
<button type="button" id="journeyEarlierBtn">Earlier</button>
|
||
<button type="submit" class="primary">Search</button>
|
||
<button type="button" id="journeyLaterBtn">Later</button>
|
||
</div>
|
||
<button type="button" id="generateItinerariesBtn">Generate travel options</button>
|
||
</form>
|
||
<div id="journeyResults" class="journey-results"></div>
|
||
<section class="itinerary-panel">
|
||
<div class="journey-title">
|
||
<span>Comparison</span>
|
||
<button type="button" id="reloadItinerariesBtn">Reload</button>
|
||
</div>
|
||
<div id="itineraryResults" class="itinerary-results muted">Generate travel options to compare route families.</div>
|
||
</section>
|
||
</section>
|
||
<div class="legend">
|
||
<span><b class="line osm"></b>OSM existing routes</span>
|
||
<span><b class="line gtfs"></b>GTFS covered routes</span>
|
||
<span><b class="line missing"></b>GTFS missing OSM match</span>
|
||
<span><b class="dot stops"></b>Stops / stations / terminals</span>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
<div id="overlay" class="overlay" hidden>
|
||
<section class="overlay-panel">
|
||
<div class="overlay-title">
|
||
<h2 id="overlayTitle">Candidates</h2>
|
||
<button id="overlayCloseBtn">Close</button>
|
||
</div>
|
||
<div id="overlayContent"></div>
|
||
</section>
|
||
</div>
|
||
|
||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
|
||
<script src="/static/app.js?v=20260701-harmonizer-module"></script>
|
||
</body>
|
||
</html>
|