diff --git a/app.py b/app.py index 5ae8691..1527c00 100644 --- a/app.py +++ b/app.py @@ -25,6 +25,7 @@ import scraper.eurostar as eurostar_scraper import scraper.gwr_fares as gwr_fares_scraper import scraper.realtime_trains as rtt_scraper from trip_planner import ( + INBOUND_MAX_CONNECTION_MINUTES, INBOUND_MIN_CONNECTION_MINUTES, combine_inbound_trips, combine_trips, @@ -34,8 +35,6 @@ from trip_planner import ( import cache import circle_line -CONNECTION_WINDOW_MINUTES = 90 - RTT_PADDINGTON_URL = ( "https://www.realtimetrains.co.uk/search/detailed/" "gb-nr:PAD/from/gb-nr:{crs}/{date}/0000-2359" @@ -89,31 +88,47 @@ DESTINATION_OPTIONS = [ {"slug": "cologne", "city": "Cologne", "destination": "Cologne Hbf"}, ] -DESTINATIONS = {option["slug"]: option["destination"] for option in DESTINATION_OPTIONS} +DESTINATIONS = { + option["slug"]: option["destination"] for option in DESTINATION_OPTIONS +} @app.route("/") def index() -> ResponseReturnValue: today = date.today().isoformat() - default_min = _get_default_min_connection() + default_min, default_max = _get_defaults() return render_template( "index.html", destination_options=DESTINATION_OPTIONS, today=today, stations=STATIONS, default_min_connection=default_min, + default_max_connection=default_max, valid_min_connections=sorted(VALID_MIN_CONNECTIONS), - default_inbound_min_connection=INBOUND_MIN_CONNECTION_MINUTES, - valid_inbound_min_connections=sorted(VALID_INBOUND_MIN_CONNECTIONS), - valid_inbound_return_min_connections=sorted( - VALID_INBOUND_RETURN_MIN_CONNECTIONS - ), + valid_max_connections=sorted(VALID_MAX_CONNECTIONS), + default_return_date=(date.today() + timedelta(days=7)).isoformat(), ) 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"} VALID_ES_CLASSES = {"standard", "plus"} @@ -269,12 +284,11 @@ def _eurostar_price_status(price: Any, seats: Any) -> str | None: return "price_not_returned" -def _get_default_min_connection() -> int: - return app.config["DEFAULT_MIN_CONNECTION"] - - -def _max_connection_for(min_connection: int) -> int: - return min_connection + CONNECTION_WINDOW_MINUTES +def _get_defaults() -> tuple[int, int]: + return ( + app.config["DEFAULT_MIN_CONNECTION"], + app.config["DEFAULT_MAX_CONNECTION"], + ) def _parse_connection(raw: str | None, default: int, valid_set: set[int]) -> int: @@ -375,15 +389,9 @@ def _results_url( **params, ) if journey_type == "inbound": - return url_for( - "back_results", - station_crs=station_crs, - slug=slug, - travel_date=travel_date, - **params, - ) + params["journey_type"] = "inbound" return url_for( - "out_results", + "results", station_crs=station_crs, slug=slug, travel_date=travel_date, @@ -403,21 +411,22 @@ def search() -> ResponseReturnValue: if station_crs not in STATION_BY_CRS: station_crs = "BRI" if journey_type == "inbound": - default_min = INBOUND_MIN_CONNECTION_MINUTES - valid_min = VALID_INBOUND_MIN_CONNECTIONS + default_min, default_max = ( + INBOUND_MIN_CONNECTION_MINUTES, + INBOUND_MAX_CONNECTION_MINUTES, + ) + valid_min, valid_max = ( + VALID_INBOUND_MIN_CONNECTIONS, + VALID_INBOUND_MAX_CONNECTIONS, + ) else: - default_min = _get_default_min_connection() - valid_min = VALID_MIN_CONNECTIONS - min_connection_arg = ( - request.args.get("min_connection_back") - if journey_type == "inbound" - else request.args.get("min_connection") + default_min, default_max = _get_defaults() + valid_min, valid_max = VALID_MIN_CONNECTIONS, VALID_MAX_CONNECTIONS + min_conn = _parse_connection( + request.args.get("min_connection"), default_min, valid_min ) - min_conn = _parse_connection(min_connection_arg, default_min, valid_min) - inbound_min_conn = _parse_connection( - request.args.get("min_connection_in"), - INBOUND_MIN_CONNECTION_MINUTES, - VALID_INBOUND_RETURN_MIN_CONNECTIONS, + max_conn = _parse_connection( + request.args.get("max_connection"), default_max, valid_max ) nr_class = request.args.get("nr_class", DEFAULT_NR_CLASS) if nr_class not in VALID_NR_CLASSES: @@ -446,12 +455,7 @@ def search() -> ResponseReturnValue: journey_type=journey_type, return_date=return_date if journey_type == "return" else None, min_connection=None if min_conn == default_min else min_conn, - min_connection_in=( - None - if journey_type != "return" - or inbound_min_conn == INBOUND_MIN_CONNECTION_MINUTES - else inbound_min_conn - ), + max_connection=None if max_conn == default_max else max_conn, nr_class=None if nr_class == DEFAULT_NR_CLASS else nr_class, es_class=None if es_class == DEFAULT_ES_CLASS else es_class, ) @@ -459,17 +463,18 @@ def search() -> ResponseReturnValue: return redirect(url_for("index")) -@app.route("/results///out/") -def out_results(station_crs: str, slug: str, travel_date: str) -> ResponseReturnValue: - return _results(station_crs, slug, travel_date, "outbound", None) +@app.route("/results///") +def results(station_crs: str, slug: str, travel_date: str) -> ResponseReturnValue: + return _results( + station_crs, + slug, + travel_date, + request.args.get("journey_type", "outbound"), + request.args.get("return_date"), + ) -@app.route("/results///back/") -def back_results(station_crs: str, slug: str, travel_date: str) -> ResponseReturnValue: - return _results(station_crs, slug, travel_date, "inbound", None) - - -@app.route("/results///return//") +@app.route("/results////return/") def return_results( station_crs: str, slug: str, travel_date: str, return_date: str ) -> ResponseReturnValue: @@ -502,15 +507,23 @@ def _results( return redirect(url_for("index")) if journey_type == "inbound": - default_min = INBOUND_MIN_CONNECTION_MINUTES - valid_min = VALID_INBOUND_MIN_CONNECTIONS + default_min, default_max = ( + INBOUND_MIN_CONNECTION_MINUTES, + INBOUND_MAX_CONNECTION_MINUTES, + ) + valid_min, valid_max = ( + VALID_INBOUND_MIN_CONNECTIONS, + VALID_INBOUND_MAX_CONNECTIONS, + ) else: - default_min = _get_default_min_connection() - valid_min = VALID_MIN_CONNECTIONS + default_min, default_max = _get_defaults() + valid_min, valid_max = VALID_MIN_CONNECTIONS, VALID_MAX_CONNECTIONS min_connection = _parse_connection( request.args.get("min_connection"), default_min, valid_min ) - max_connection = _max_connection_for(min_connection) + max_connection = _parse_connection( + request.args.get("max_connection"), default_max, valid_max + ) nr_class = request.args.get("nr_class", DEFAULT_NR_CLASS) if nr_class not in VALID_NR_CLASSES: nr_class = DEFAULT_NR_CLASS @@ -569,6 +582,7 @@ def _results( departure_station_name=departure_station_name, journey_type=journey_type, travel_date_display=travel_date_display, + return_date=return_date, return_date_display=return_date_display, stream_url=_results_url( station_crs=station_crs, @@ -665,7 +679,7 @@ def _results( section_max_connection = max_connection if journey_type == "return" and direction == "inbound": section_min_connection = inbound_min_connection - section_max_connection = _max_connection_for(inbound_min_connection) + section_max_connection = INBOUND_MAX_CONNECTION_MINUTES rtt_direction = ( "to_paddington" if direction == "outbound" else "from_paddington" ) @@ -733,8 +747,6 @@ def _results( cached_walkon = ( exact_walkon if exact_walkon is not None else get_cached(walkon_weekday_key) ) - if isinstance(cached_walkon, dict): - gwr_fares = cached_walkon if direction == "outbound": trips = combine_trips( @@ -882,6 +894,7 @@ def _results( rtt_station_url = RTT_STATION_URL.format(crs=station_crs, date=travel_date) url_min = None if min_connection == default_min else min_connection + url_max = None if max_connection == default_max else max_connection url_nr = None if nr_class == DEFAULT_NR_CLASS else nr_class url_es = None if es_class == DEFAULT_ES_CLASS else es_class @@ -890,6 +903,7 @@ def _results( "journey_type": journey_type, "return_date": return_date, "min_connection": url_min, + "max_connection": url_max, "min_connection_in": ( None if inbound_min_connection == INBOUND_MIN_CONNECTION_MINUTES @@ -913,6 +927,7 @@ def _results( "journey_type": journey_type, "return_date": return_date, "min_connection": url_min, + "max_connection": url_max, "nr_class": url_nr, "es_class": url_es, } @@ -985,7 +1000,7 @@ def _results( "id": "inbound", "direction": "inbound", "min_connection": inbound_min_connection, - "max_connection": _max_connection_for(inbound_min_connection), + "max_connection": INBOUND_MAX_CONNECTION_MINUTES, }, ] shell_nr_classes = {"outbound": nr_class_out, "inbound": nr_class_in} @@ -1012,6 +1027,8 @@ def _results( journey_type=journey_type, destination=destination, departure_station_name=departure_station_name, + travel_date=travel_date, + return_date=return_date, travel_date_display=travel_date_display, return_date_display=return_date_display, slug=slug, @@ -1033,9 +1050,12 @@ def _results( rtt_url=rtt_url, rtt_station_url=rtt_station_url, min_connection=min_connection, + max_connection=max_connection, default_min_connection=default_min, + default_max_connection=default_max, default_inbound_min_connection=INBOUND_MIN_CONNECTION_MINUTES, valid_min_connections=sorted(valid_min), + valid_max_connections=sorted(valid_max), inbound_min_connection=inbound_min_connection, valid_inbound_return_min_connections=sorted( VALID_INBOUND_RETURN_MIN_CONNECTIONS @@ -1071,8 +1091,6 @@ def _results( section=section, destination=destination, departure_station_name=departure_station_name, - nr_classes=shell_nr_classes, - es_classes=shell_es_classes, ) yield f"data: {json.dumps({'type': 'section', 'id': section_id, 'html': section_html, 'trip_fares': _section_trip_fares(section), 'advance_fares': section['advance_fares'], 'walkon_cached_fares': section.get('cached_walkon_fares'), 'walkon_api_url': section['walkon_api_url'], 'advance_api_url': section['advance_api_url'], 'advance_stream_url': section['advance_stream_url']})}\n\n" @@ -1090,7 +1108,7 @@ def _results( station_crs=station_crs, slug=slug, travel_date=travel_date, - path_journey_type="back" if journey_type == "inbound" else "out", + journey_type=journey_type if journey_type == "inbound" else None, ) summary_html = _build_summary_html( @@ -1163,6 +1181,7 @@ def _results( rtt_station_url = RTT_STATION_URL.format(crs=station_crs, date=travel_date) url_min = None if min_connection == default_min else min_connection + url_max = None if max_connection == default_max else max_connection url_nr = None if nr_class == DEFAULT_NR_CLASS else nr_class url_es = None if es_class == DEFAULT_ES_CLASS else es_class common_url_args: dict[str, Any] @@ -1171,6 +1190,7 @@ def _results( "journey_type": journey_type, "return_date": return_date, "min_connection": url_min, + "max_connection": url_max, "min_connection_in": ( None if inbound_min_connection == INBOUND_MIN_CONNECTION_MINUTES @@ -1186,6 +1206,7 @@ def _results( "journey_type": journey_type, "return_date": return_date, "min_connection": url_min, + "max_connection": url_max, "nr_class": url_nr, "es_class": url_es, } @@ -1310,16 +1331,25 @@ def _results( station_crs=station_crs, slug=slug, travel_date=travel_date, - path_journey_type="back" if journey_type == "inbound" else "out", + journey_type=journey_type if journey_type == "inbound" else None, ) return render_template( "results.html", sections=sections, + trips=sections[0]["trips"] if sections else [], + result_rows=sections[0]["rows"] if sections else [], + unreachable_morning_services=[], + destinations=DESTINATIONS, destination=destination, + travel_date=travel_date, + return_date=return_date, journey_type=journey_type, slug=slug, + station_crs=station_crs, departure_station_name=departure_station_name, + prev_date=prev_date, + next_date=next_date, prev_results_url=prev_results_url, next_results_url=next_results_url, prev_outbound_url=prev_outbound_url, @@ -1340,7 +1370,15 @@ def _results( rtt_url=rtt_url, rtt_station_url=rtt_station_url, min_connection=min_connection, + max_connection=max_connection, default_min_connection=default_min, + default_max_connection=default_max, + url_min_connection=url_min, + url_max_connection=url_max, + nr_class=nr_class, + es_class=es_class, + url_nr_class=url_nr, + url_es_class=url_es, nr_classes=nr_classes, es_classes=es_classes, nr_classes_json=json.dumps(nr_classes), @@ -1353,7 +1391,14 @@ def _results( advance_api_urls_json=json.dumps(advance_api_urls), advance_stream_urls_json=json.dumps(advance_stream_urls), timetable_refresh_url=timetable_refresh_url, + advance_fares_api_url=url_for( + "api_advance_fares", station_crs=station_crs, travel_date=travel_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( @@ -1435,20 +1480,15 @@ def api_walkon_fares( return jsonify({"error": str(e)}), 500 -@app.route( - "/api/results_refresh////" -) -def api_results_refresh( - station_crs: str, slug: str, path_journey_type: str, travel_date: str -) -> Response: - if path_journey_type not in {"out", "back"}: - abort(404) - journey_type = "inbound" if path_journey_type == "back" else "outbound" - return _api_results_refresh(station_crs, slug, travel_date, None, journey_type) +@app.route("/api/results_refresh///") +def api_results_refresh(station_crs: str, slug: str, travel_date: str) -> Response: + return _api_results_refresh( + station_crs, slug, travel_date, request.args.get("return_date") + ) @app.route( - "/api/results_refresh///return//" + "/api/results_refresh////return/" ) def api_return_results_refresh( station_crs: str, slug: str, travel_date: str, return_date: str diff --git a/config/default.py b/config/default.py index c249656..3d9c4e2 100644 --- a/config/default.py +++ b/config/default.py @@ -11,3 +11,4 @@ CIRCLE_LINE_XML = os.path.join(TFL_DATA_DIR, "output_txc_01CIR_.xml") # Default connection window (minutes) between Paddington arrival and St Pancras departure DEFAULT_MIN_CONNECTION = 70 +DEFAULT_MAX_CONNECTION = 150 diff --git a/templates/base.html b/templates/base.html index 4afdefd..5aeafa9 100644 --- a/templates/base.html +++ b/templates/base.html @@ -342,7 +342,7 @@ tr.row-selectable:hover:not(.row-selected) { filter: brightness(0.97); } /* Journey flow arrow between column headers */ - .results-table thead th.flow-step { position: sticky; padding-right: 1.4rem; } + .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%); diff --git a/templates/index.html b/templates/index.html index a3e495c..8d29dfe 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,8 +1,6 @@ {% extends "base.html" %} {% block content %}