diff --git a/app.py b/app.py
index aae6b8c..b29439c 100644
--- a/app.py
+++ b/app.py
@@ -90,6 +90,7 @@ def index():
VALID_MIN_CONNECTIONS = {45, 50, 60, 70, 80, 90, 100, 110, 120}
VALID_MAX_CONNECTIONS = {60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180}
VALID_INBOUND_MIN_CONNECTIONS = {20, 30, 40, 45, 50, 60, 70, 80, 90, 100, 110, 120}
+VALID_INBOUND_RETURN_MIN_CONNECTIONS = {30, 40, 50, 60}
VALID_INBOUND_MAX_CONNECTIONS = {60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180}
VALID_JOURNEY_TYPES = {"outbound", "inbound", "return"}
VALID_NR_CLASSES = {'walkon', 'advance_std', 'advance_1st'}
@@ -357,6 +358,7 @@ def _results(station_crs, slug, travel_date, journey_type, return_date):
if es_class not in VALID_ES_CLASSES:
es_class = DEFAULT_ES_CLASS
+ inbound_min_connection = INBOUND_MIN_CONNECTION_MINUTES
if journey_type == "return":
def _p(raw, default, valid):
return raw if raw in valid else default
@@ -364,6 +366,11 @@ def _results(station_crs, slug, travel_date, journey_type, return_date):
nr_class_in = _p(request.args.get("nr_class_in"), DEFAULT_NR_CLASS, VALID_NR_CLASSES)
es_class_out = _p(request.args.get("es_class_out"), DEFAULT_ES_CLASS, VALID_ES_CLASSES)
es_class_in = _p(request.args.get("es_class_in"), DEFAULT_ES_CLASS, VALID_ES_CLASSES)
+ inbound_min_connection = _parse_connection(
+ request.args.get("min_connection_in"),
+ INBOUND_MIN_CONNECTION_MINUTES,
+ VALID_INBOUND_RETURN_MIN_CONNECTIONS,
+ )
else:
nr_class_out = nr_class_in = nr_class
es_class_out = es_class_in = es_class
@@ -464,7 +471,7 @@ def _results(station_crs, slug, travel_date, journey_type, return_date):
section_min_connection = min_connection
section_max_connection = max_connection
if journey_type == "return" and direction == "inbound":
- section_min_connection = INBOUND_MIN_CONNECTION_MINUTES
+ section_min_connection = inbound_min_connection
section_max_connection = INBOUND_MAX_CONNECTION_MINUTES
rtt_direction = "to_paddington" if direction == "outbound" else "from_paddington"
rtt_cache_key = _nr_exact_cache_key(rtt_direction, station_crs, section_date)
@@ -672,6 +679,7 @@ def _results(station_crs, slug, travel_date, journey_type, return_date):
"return_date": return_date,
"min_connection": url_min,
"max_connection": url_max,
+ "min_connection_in": None if inbound_min_connection == INBOUND_MIN_CONNECTION_MINUTES else inbound_min_connection,
"nr_class_out": None if nr_class_out == DEFAULT_NR_CLASS else nr_class_out,
"nr_class_in": None if nr_class_in == DEFAULT_NR_CLASS else nr_class_in,
"es_class_out": None if es_class_out == DEFAULT_ES_CLASS else es_class_out,
@@ -860,6 +868,9 @@ def _results(station_crs, slug, travel_date, journey_type, return_date):
advance_fares_stream_url=url_for("api_advance_fares_stream", station_crs=station_crs, travel_date=travel_date),
valid_min_connections=sorted(valid_min),
valid_max_connections=sorted(valid_max),
+ inbound_min_connection=inbound_min_connection,
+ default_inbound_min_connection=INBOUND_MIN_CONNECTION_MINUTES,
+ valid_inbound_return_min_connections=sorted(VALID_INBOUND_RETURN_MIN_CONNECTIONS),
)
diff --git a/templates/base.html b/templates/base.html
index bf73706..242196a 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -182,10 +182,47 @@
}
@media (max-width: 640px) {
- .card {
- padding: 1.25rem;
+ .card { padding: 1.25rem; }
+
+ /* Convert results table to a 2-column card layout per row */
+ .results-table, .results-table tbody { display: block; }
+ .results-table thead { display: none; }
+ .results-table tr {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ border-bottom: 2px solid #e2e8f0;
+ padding: 0.1rem 0;
}
- .col-transfer { display: none; }
+ .results-table td { padding: 0.35rem 0.45rem; font-size: 0.8rem; border-bottom: none; }
+
+ /* First journey leg (NR outbound / Eurostar inbound) */
+ .results-table td:nth-child(1) { grid-column: 1; grid-row: 1; }
+ /* Transfer column: hidden */
+ .col-transfer { display: none !important; }
+ /* Second journey leg */
+ .results-table td:nth-child(3) { grid-column: 2; grid-row: 1; }
+ /* Total: spans both columns, right-aligned */
+ .results-table td:nth-child(4) {
+ grid-column: 1 / -1; grid-row: 2;
+ text-align: right;
+ border-top: 1px solid #e2e8f0;
+ padding: 0.25rem 0.45rem 0.3rem;
+ }
+
+ /* Hide non-essential detail on mobile */
+ .mobile-hide { display: none !important; }
+ .fare-seats { display: none !important; }
+
+ /* Show connection time hint */
+ .mobile-conn { display: block !important; }
+
+ /* Flow arrow: hide on mobile */
+ .results-table thead th.flow-step::after { display: none; }
+ .results-table thead th.flow-step { padding-right: 0; }
+
+ /* Selection bar: smaller on mobile */
+ #selection-bar { padding: 0.5rem 0.75rem; font-size: 0.8rem; }
+ .sel-totals { gap: 0.75rem; }
}
a { color: #00539f; }
@@ -277,6 +314,41 @@
.row-slow { background: #fff5f5; }
.row-alt { background: #f7fafc; }
.row-unreachable { background: #f7fafc; color: #a0aec0; }
+ .row-selected { background: #ebf8ff !important; }
+ tr.row-selectable { cursor: pointer; }
+ tr.row-selectable:hover:not(.row-selected) { filter: brightness(0.97); }
+
+ /* Journey flow arrow between column headers */
+ .results-table thead th.flow-step { position: relative; padding-right: 1.4rem; }
+ .results-table thead th.flow-step::after {
+ content: '›';
+ position: absolute; right: 0.2rem; top: 50%; transform: translateY(-50%);
+ color: #cbd5e0; font-size: 1.5rem; font-weight: 300; line-height: 1;
+ }
+
+ /* Mobile: hidden by default, shown on mobile */
+ .mobile-conn { display: none; }
+ .fare-seats { display: inline; }
+
+ /* Selection summary bar */
+ #selection-bar {
+ display: none; position: fixed; bottom: 0; left: 0; right: 0;
+ background: #fff; border-top: 2px solid #00539f;
+ padding: 0.65rem 1rem;
+ box-shadow: 0 -2px 10px rgba(0,0,0,0.12); z-index: 200;
+ font-size: 0.88rem;
+ }
+ .sel-bar-inner {
+ max-width: 1100px; margin: 0 auto;
+ display: flex; justify-content: space-between; align-items: center;
+ flex-wrap: wrap; gap: 0.5rem;
+ }
+ .sel-totals { display: flex; gap: 1.25rem; align-items: center; flex-wrap: wrap; }
+ .sel-clear {
+ background: none; border: 1px solid #cbd5e0; border-radius: 4px;
+ padding: 0.2rem 0.6rem; font-size: 0.8rem; cursor: pointer; color: #718096;
+ }
+ .sel-clear:hover { background: #f0f4f8; }
/* Empty state */
.empty-state { color: #4a5568; text-align: center; padding: 3rem 2rem; }
diff --git a/templates/index.html b/templates/index.html
index 3a3317f..898a6e4 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -1,8 +1,75 @@
{% extends "base.html" %}
{% block content %}
+
+
+
-
- Outbound / single date
-
-
-
-
-
@@ -101,60 +150,330 @@
-
- Search journeys
-
+ Search journeys
+
{% endblock %}
diff --git a/templates/results.html b/templates/results.html
index d176481..afe5d03 100644
--- a/templates/results.html
+++ b/templates/results.html
@@ -79,6 +79,16 @@
{% for section in sections %}
{{ 'Outbound' if section.direction == 'outbound' else 'Return' }}:
+ {% if section.direction == 'inbound' %}
+
+ Min connection:
+
+ {% for mins in valid_inbound_return_min_connections %}
+ {{ mins }} min
+ {% endfor %}
+
+
+ {% endif %}
NR:
@@ -129,6 +139,7 @@
const RESULTS_BASE = '{{ results_base_url }}';
const DEFAULT_MIN_CONN = {{ default_min_connection }};
const DEFAULT_MAX_CONN = {{ default_max_connection }};
+ const DEFAULT_MIN_CONN_IN = {{ default_inbound_min_connection }};
let TRIP_FARES = {{ trip_fares_json | safe }};
let ADVANCE_FARES = {{ advance_fares_json | safe }};
let WALKON_API_URLS = {{ walkon_api_urls_json | safe }};
@@ -141,6 +152,8 @@
let currentEsClasses = {{ es_classes_json | safe }};
const SECTION_DIRECTIONS = {{ section_directions_json | safe }};
let advanceLoadingSections = {};
+ let walkonLoadingSections = {};
+ let selectedRowKeys = {};
function updateAdvanceLoadingStatus() {
var loading = Object.keys(advanceLoadingSections).some(function(sectionId) {
@@ -156,6 +169,11 @@
var params = [];
if (min !== DEFAULT_MIN_CONN) params.push('min_connection=' + min);
if (max !== DEFAULT_MAX_CONN) params.push('max_connection=' + max);
+ var minInEl = document.getElementById('min_conn_in_select');
+ if (minInEl) {
+ var minIn = parseInt(minInEl.value);
+ if (minIn !== DEFAULT_MIN_CONN_IN) params.push('min_connection_in=' + minIn);
+ }
var sectionIds = Object.keys(currentNrClasses);
if (sectionIds.length === 1) {
var nrCls = currentNrClasses[sectionIds[0]];
@@ -172,6 +190,12 @@
if (esC !== 'standard') params.push('es_class' + suffix + '=' + esC);
});
}
+ for (var _sid in selectedRowKeys) {
+ var _sel = selectedRowKeys[_sid];
+ if (!_sel || !SECTION_DIRECTIONS[_sid]) continue;
+ var _pname = SECTION_DIRECTIONS[_sid] === 'outbound' ? 'out' : 'ret';
+ params.push(_pname + '=' + encodeURIComponent(_sel.slice(_sid.length + 1)));
+ }
return params.length ? RESULTS_BASE + '?' + params.join('&') : RESULTS_BASE;
}
@@ -205,7 +229,7 @@
function fareHtml(fare) {
return '
' + fmtPrice(fare.price) + ' '
+ (fare.ticket ? '
' + fare.ticket + ' ' : '')
- + (fare.seats != null ? '
' + fare.seats + ' at this price ' : '');
+ + (fare.seats != null ? '
' + fare.seats + ' at this price ' : '');
}
function mergeAdvanceFares(sectionId, fares) {
@@ -376,17 +400,19 @@
var esStdEl = tr.querySelector('.es-standard');
var esPlusEl = tr.querySelector('.es-plus');
if (esStdEl) {
- esStdEl.innerHTML = row.es_standard ? fareHtml(row.es_standard) : '
\u2013 ';
+ esStdEl.innerHTML = '
Std ' + (row.es_standard ? fareHtml(row.es_standard) : '
\u2013 ');
esStdEl.classList.toggle('fare-inactive', esClass !== 'standard');
}
if (esPlusEl) {
- esPlusEl.innerHTML = row.es_plus ? fareHtml(row.es_plus) : '
\u2013 ';
+ esPlusEl.innerHTML = '
SP ' + (row.es_plus ? fareHtml(row.es_plus) : '
\u2013 ');
esPlusEl.classList.toggle('fare-inactive', esClass !== 'plus');
}
var totalSpan = tr.querySelector('.total-price');
if (totalSpan) {
- if (key in totals) {
+ if (key.indexOf(':unreachable:') !== -1) {
+ totalSpan.innerHTML = '
No rail connection ';
+ } else if (key in totals) {
var total = totals[key];
var html = '
' + fmtPrice(total);
if (minTotal !== null && maxTotal !== null) {
@@ -395,42 +421,153 @@
}
html += ' ';
totalSpan.innerHTML = html;
+ } else if (!nrFare && walkonLoadingSections[row.section]) {
+ totalSpan.innerHTML = '';
+ } else if (!nrFare) {
+ totalSpan.innerHTML = '
NR fare not available ';
} else {
- var missing = !nrFare ? 'No NR fare' : 'No Eurostar fare';
- totalSpan.innerHTML = '
' + missing + ' ';
+ totalSpan.innerHTML = '
No Eurostar price ';
}
}
});
+ updateSelectionBar();
}
function loadWalkonFares() {
var urls = WALKON_API_URLS;
- var pending = Object.keys(urls).length;
- if (!pending) return;
- function done() {
+ var ids = Object.keys(urls);
+ if (!ids.length) return;
+ /* outbound first, then inbound */
+ ids.sort(function(a, b) {
+ return (SECTION_DIRECTIONS[a] === 'outbound' ? 0 : 1) -
+ (SECTION_DIRECTIONS[b] === 'outbound' ? 0 : 1);
+ });
+ ids.forEach(function(sid) { walkonLoadingSections[sid] = true; });
+ var pending = ids.length;
+ function done(id) {
+ walkonLoadingSections[id] = false;
if (--pending === 0) {
var el = document.getElementById('walkon-loading');
if (el) el.style.display = 'none';
}
}
- for (var sectionId in urls) {
- (function(id, url) {
- fetch(url).then(function(r) { return r.json(); }).then(function(fares) {
- mergeWalkonFares(id, fares);
- updateDisplay();
- done();
- }).catch(done);
- })(sectionId, urls[sectionId]);
+ /* sequential: each fetch starts only after the previous one finishes */
+ ids.reduce(function(chain, id) {
+ return chain.then(function() {
+ return fetch(urls[id])
+ .then(function(r) { return r.json(); })
+ .then(function(fares) { mergeWalkonFares(id, fares); done(id); updateDisplay(); })
+ .catch(function() { done(id); updateDisplay(); });
+ });
+ }, Promise.resolve());
+ }
+
+ function initSelectionFromUrl() {
+ var params = new URLSearchParams(window.location.search);
+ for (var sid in SECTION_DIRECTIONS) {
+ var dir = SECTION_DIRECTIONS[sid];
+ var val = params.get(dir === 'outbound' ? 'out' : 'ret');
+ if (val) {
+ var rowKey = sid + ':' + val;
+ if (TRIP_FARES[rowKey]) selectedRowKeys[sid] = rowKey;
+ }
+ }
+ }
+
+ function selectRow(tr) {
+ var key = tr.getAttribute('data-row-key');
+ if (!key || key.indexOf(':unreachable:') !== -1) return;
+ var row = TRIP_FARES[key];
+ if (!row) return;
+ selectedRowKeys[row.section] = (selectedRowKeys[row.section] === key) ? null : key;
+ updateRowHighlights();
+ updateSelectionBar();
+ history.replaceState(null, '', buildUrl());
+ }
+
+ function clearSelection() {
+ selectedRowKeys = {};
+ updateRowHighlights();
+ updateSelectionBar();
+ history.replaceState(null, '', buildUrl());
+ }
+
+ function updateRowHighlights() {
+ document.querySelectorAll('tr[data-row-key]').forEach(function(tr) {
+ var key = tr.getAttribute('data-row-key');
+ var row = TRIP_FARES[key];
+ if (!row) return;
+ tr.classList.toggle('row-selected', selectedRowKeys[row.section] === key);
+ });
+ }
+
+ function updateSelectionBar() {
+ var bar = document.getElementById('selection-bar');
+ if (!bar) return;
+ var allSids = Object.keys(SECTION_DIRECTIONS);
+ var activeSids = allSids.filter(function(sid) { return selectedRowKeys[sid]; });
+ if (activeSids.length === 0) { bar.style.display = 'none'; return; }
+ bar.style.display = 'block';
+
+ var totalNr = 0, totalEs = 0, totalCircle = 0, allPrices = true;
+ var parts = [];
+ activeSids.forEach(function(sid) {
+ var rowKey = selectedRowKeys[sid];
+ var row = TRIP_FARES[rowKey];
+ if (!row) return;
+ var nrFare = currentNrFare(row);
+ var esClass = currentEsClasses[sid] || 'standard';
+ var esFare = esClass === 'standard' ? row.es_standard : row.es_plus;
+ if (nrFare) totalNr += nrFare.price; else allPrices = false;
+ if (esFare) totalEs += esFare.price; else allPrices = false;
+ totalCircle += row.circle_fare || 0;
+ var kp = rowKey.split(':');
+ // outbound key: section:NRdep:ESdep — show NR dep (Bristol)
+ // inbound key: section:NRdep:ESdep — show ES dep (destination, CET)
+ var depTime = SECTION_DIRECTIONS[sid] === 'outbound'
+ ? kp[1] + ':' + kp[2]
+ : kp[3] + ':' + kp[4];
+ parts.push((SECTION_DIRECTIONS[sid] === 'outbound' ? 'Out ' : 'Ret ') + depTime);
+ });
+
+ var descEl = document.getElementById('sel-desc');
+ if (descEl) descEl.textContent = parts.join(' · ');
+
+ var hintEl = document.getElementById('sel-hint');
+ if (hintEl) {
+ if (allSids.length > 1 && activeSids.length < allSids.length) {
+ var missingDir = SECTION_DIRECTIONS[allSids.filter(function(s) { return !selectedRowKeys[s]; })[0]];
+ hintEl.textContent = 'Select a ' + (missingDir === 'outbound' ? 'outbound' : 'return') + ' journey to see combined total';
+ hintEl.style.display = '';
+ } else {
+ hintEl.style.display = 'none';
+ }
+ }
+
+ var grandTotal = totalNr + totalEs + totalCircle;
+ var nrEl = document.getElementById('sel-nr');
+ var esEl = document.getElementById('sel-es');
+ var grandEl = document.getElementById('sel-grand');
+ if (nrEl) nrEl.innerHTML = 'NR
' + (allPrices ? fmtPrice(totalNr) : '–') + ' ';
+ if (esEl) esEl.innerHTML = 'Eurostar
' + (allPrices ? fmtPrice(totalEs) : '–') + ' ';
+ if (grandEl) {
+ var label = (activeSids.length === allSids.length && allSids.length > 1) ? 'Grand total' : 'Total';
+ var priceHtml = allPrices
+ ? '
' + fmtPrice(grandTotal) + ' '
+ : '
– ';
+ grandEl.innerHTML = label + ' ' + priceHtml;
}
}
function initialiseResultsPage() {
+ initSelectionFromUrl();
var needsAdvance = Object.keys(currentNrClasses).some(function(sid) {
var c = currentNrClasses[sid];
return c === 'advance_std' || c === 'advance_1st';
});
if (needsAdvance) loadMissingAdvanceFares();
updateDisplay();
+ updateRowHighlights();
loadWalkonFares();
startTimetableRefresh();
}
@@ -548,15 +685,15 @@
{% if section.direction == 'inbound' %}
- Eurostar{{ destination }} → St Pancras
- TransferSt Pancras → Paddington
+ Eurostar{{ destination }} → St Pancras
+ TransferSt Pancras → Paddington
National RailPaddington → {{ departure_station_name }}
{% else %}
- National Rail{{ departure_station_name }} → Paddington
- TransferPaddington → St Pancras
+ National Rail{{ departure_station_name }} → Paddington
+ TransferPaddington → St Pancras
EurostarSt Pancras → {{ destination }}
{% endif %}
- Total
+ Totalclick row to select
@@ -577,8 +714,10 @@
{% else %}
{% set row_class = '' %}
{% endif %}
-
+
{% if row.row_type == 'trip' %}
{% if section.direction == 'inbound' %}
@@ -591,8 +730,9 @@
{%- if row.train_number %}{{ row.train_number }}{% endif %}
{% endif %}
- {% if row.eurostar_price is not none %}£{{ "%.2f"|format(row.eurostar_price) }} {% endif %}
- {% if row.eurostar_plus_price is not none %}£{{ "%.2f"|format(row.eurostar_plus_price) }} {% endif %}
+ Std {% if row.eurostar_price is not none %} £{{ "%.2f"|format(row.eurostar_price) }} {% endif %}
+ SP {% if row.eurostar_plus_price is not none %} £{{ "%.2f"|format(row.eurostar_plus_price) }} {% endif %}
+ then {{ row.connection_duration }} to station{% if row.connection_minutes < 45 %} ⚠️{% endif %}
{{ row.connection_duration }}{% if row.connection_minutes < 45 %} ⚠️ {% endif %}
@@ -612,7 +752,7 @@
{{ row.depart_paddington }} → {{ row.arrive_uk_station }}
({{ row.gwr_duration }})
{% if row.headcode or row.arrive_platform %}
- {{ row.headcode }}{% if row.headcode and row.arrive_platform %} · {% endif %}{% if row.arrive_platform %}Plat {{ row.arrive_platform }}{% endif %}
+ {{ row.headcode }}{% if row.headcode and row.arrive_platform %} · {% endif %}{% if row.arrive_platform %}Plat {{ row.arrive_platform }}{% endif %}
{% endif %}
{% if row.ticket_price is not none %}£{{ "%.2f"|format(row.ticket_price) }} {% endif %}
@@ -623,11 +763,12 @@
{{ row.depart_bristol }} → {{ row.arrive_paddington }}
({{ row.gwr_duration }})
{% if row.headcode or row.arrive_platform %}
- {{ row.headcode }}{% if row.headcode and row.arrive_platform %} · {% endif %}{% if row.arrive_platform %}Plat {{ row.arrive_platform }}{% endif %}
+ {{ row.headcode }}{% if row.headcode and row.arrive_platform %} · {% endif %}{% if row.arrive_platform %}Plat {{ row.arrive_platform }}{% endif %}
{% endif %}
{% if row.ticket_price is not none %}£{{ "%.2f"|format(row.ticket_price) }} {% endif %}
+ then {{ row.connection_duration }} to Eurostar{% if row.connection_minutes < 80 %} ⚠️{% endif %}
{{ row.connection_duration }}{% if row.connection_minutes < 80 %} ⚠️ {% endif %}
@@ -649,8 +790,8 @@
{%- if row.train_number %}{{ row.train_number }}{% endif %}
{% endif %}
- {% if row.eurostar_price is not none %}£{{ "%.2f"|format(row.eurostar_price) }} {% endif %}
- {% if row.eurostar_plus_price is not none %}£{{ "%.2f"|format(row.eurostar_plus_price) }} {% endif %}
+ Std {% if row.eurostar_price is not none %} £{{ "%.2f"|format(row.eurostar_price) }} {% endif %}
+ SP {% if row.eurostar_plus_price is not none %} £{{ "%.2f"|format(row.eurostar_plus_price) }} {% endif %}
{% endif %}
@@ -665,7 +806,7 @@
{% else %}
- Too early
+ Too early
—
@@ -676,8 +817,10 @@
{{ row.depart_st_pancras }} → {{ row.arrive_destination }}
{% if row.train_number %}{{ row.train_number }} {% endif %}
{% endif %}
+
+
- No connection
+
{% endif %}
{% endfor %}
@@ -718,4 +861,19 @@
{% endif %}
+
+
+
+
+
+
+
+
+
+
+ Clear
+
+
+
+
{% endblock %}
diff --git a/trip_planner.py b/trip_planner.py
index d03b767..d858a82 100644
--- a/trip_planner.py
+++ b/trip_planner.py
@@ -44,11 +44,24 @@ def _circle_line_services(arrive_paddington: datetime) -> list[dict]:
]
-def _circle_line_services_to_paddington(arrive_st_pancras: datetime) -> list[dict]:
+PAD_WALK_FROM_UNDERGROUND_MINUTES = 5 # Circle line platform → GWR platform at Paddington
+INBOUND_COMFORTABLE_MIN_CONN = 40 # threshold above which we apply the platform walk buffer
+
+
+def _circle_line_services_to_paddington(
+ arrive_st_pancras: datetime,
+ dep_paddington: datetime | None = None,
+ min_conn_minutes: int = INBOUND_MIN_CONNECTION_MINUTES,
+) -> list[dict]:
earliest_board = arrive_st_pancras + timedelta(
minutes=KX_WALK_TO_UNDERGROUND_MINUTES
)
- services = circle_line.upcoming_services(earliest_board, count=1, direction='kx_to_pad', preceding=1)
+ if min_conn_minutes >= INBOUND_COMFORTABLE_MIN_CONN and dep_paddington is not None:
+ cutoff = dep_paddington - timedelta(minutes=PAD_WALK_FROM_UNDERGROUND_MINUTES)
+ candidates = circle_line.upcoming_services(earliest_board, count=4, direction='kx_to_pad')
+ services = [(dep, arr) for dep, arr in candidates if arr <= cutoff][:2]
+ else:
+ services = circle_line.upcoming_services(earliest_board, count=1, direction='kx_to_pad', preceding=1)
return [
{
"depart": dep.strftime(TIME_FMT),
@@ -166,8 +179,8 @@ def combine_trips(
continue
dep_bri, arr_pad, dep_stp, arr_dest = connection
- total_mins = int((arr_dest - dep_bri).total_seconds() / 60)
# Destination time is CET/CEST, departure is GMT/BST; Europe is always 1h ahead.
+ total_mins = int((arr_dest - dep_bri).total_seconds() / 60) - 60
eurostar_mins = int((arr_dest - dep_stp).total_seconds() / 60) - 60
fare = (gwr_fares or {}).get(gwr["depart_bristol"])
circle_svcs = _circle_line_services(arr_pad)
@@ -226,11 +239,11 @@ def combine_inbound_trips(
if not connection:
continue
dep_dest, arr_stp, dep_pad, arr_station = connection
- total_mins = int((arr_station - dep_dest).total_seconds() / 60)
- # Destination time is CET/CEST, arrival at London is GMT/BST.
+ # Destination time is CET/CEST, arrival at London is GMT/BST; Europe is always 1h ahead.
+ total_mins = int((arr_station - dep_dest).total_seconds() / 60) + 60
eurostar_mins = int((arr_stp - dep_dest).total_seconds() / 60) + 60
fare = (gwr_fares or {}).get(gwr["depart_paddington"])
- circle_svcs = _circle_line_services_to_paddington(arr_stp)
+ circle_svcs = _circle_line_services_to_paddington(arr_stp, dep_pad, min_connection_minutes)
trips.append(
{
"direction": "inbound",