diff --git a/agenda/trip.py b/agenda/trip.py index 9c538e1..6521ab7 100644 --- a/agenda/trip.py +++ b/agenda/trip.py @@ -130,8 +130,6 @@ def get_unbooked_flight_origin_iata( UNBOOKED_RAIL_DESTINATIONS: dict[tuple[str, str], tuple[str, str]] = { ("fr", "paris"): ("London St Pancras", "Paris Gare du Nord"), } -EUROSTAR_LONDON_STATION = "London St Pancras" -EUROSTAR_HOME_FEEDER_ROUTE = ("Bristol Temple Meads", "London Paddington") def load_station_lookup(data_dir: str) -> dict[str, StrDict]: @@ -170,71 +168,6 @@ def get_unbooked_rail_route(item: StrDict, data_dir: str) -> StrDict | None: return route -def build_train_route( - train_from: StrDict, train_to: StrDict, seen_geojson: set[str] -) -> StrDict | None: - """Build a map route between two stations.""" - geojson_filename = train_from.get("routes", {}).get(train_to["name"]) - key = "_".join(["train"] + sorted([train_from["name"], train_to["name"]])) - if not geojson_filename: - return { - "type": "train", - "key": key, - "from": latlon_tuple(train_from), - "to": latlon_tuple(train_to), - } - - if geojson_filename in seen_geojson: - return None - seen_geojson.add(geojson_filename) - - return { - "type": "train", - "key": key, - "geojson_filename": os.path.join("train_routes", geojson_filename), - } - - -def train_operator_is_eurostar(item: StrDict) -> bool: - """Return True when a train item is operated by Eurostar.""" - operator = item.get("operator") - return isinstance(operator, str) and operator.casefold() == "eurostar" - - -def is_eurostar_departure_from_london(train: StrDict) -> bool: - """Return True when a train journey departs London St Pancras on Eurostar.""" - if train.get("from") == EUROSTAR_LONDON_STATION and train_operator_is_eurostar( - train - ): - return True - - for leg in train.get("legs", []): - if leg.get("from") == EUROSTAR_LONDON_STATION and train_operator_is_eurostar( - leg - ): - return True - return False - - -def get_eurostar_home_feeder_route( - trip: Trip, data_dir: str, seen_geojson: set[str] -) -> StrDict | None: - """Return the assumed Bristol to Paddington route for London Eurostar trips.""" - if not any( - t["type"] == "train" and is_eurostar_departure_from_london(t) - for t in trip.travel - ): - return None - - stations = load_station_lookup(data_dir) - from_station = stations.get(EUROSTAR_HOME_FEEDER_ROUTE[0]) - to_station = stations.get(EUROSTAR_HOME_FEEDER_ROUTE[1]) - if from_station is None or to_station is None: - return None - - return build_train_route(from_station, to_station, seen_geojson) - - def load_travel(travel_type: str, plural: str, data_dir: str) -> list[StrDict]: """Read flight and train journeys.""" items: list[StrDict] = travel.parse_yaml(plural, data_dir) @@ -755,37 +688,6 @@ def coordinate_dict(item: StrDict, coord_type: str) -> StrDict: } -def coordinate_key(item: StrDict) -> tuple[str, str]: - """Return the de-duplication key for a coordinate item.""" - return (item["type"], item["name"]) - - -def add_coordinate_if_missing(coordinates: list[StrDict], item: StrDict) -> None: - """Append a coordinate item if the same type/name is not already present.""" - key = coordinate_key(item) - if any(coordinate_key(existing) == key for existing in coordinates): - return - coordinates.append(item) - - -def add_coordinates_for_eurostar_home_feeder( - trip: Trip, coordinates: list[StrDict], data_dir: str -) -> None: - """Add the assumed Bristol station pin for London Eurostar feeder travel.""" - if not any( - t["type"] == "train" and is_eurostar_departure_from_london(t) - for t in trip.travel - ): - return - - stations = load_station_lookup(data_dir) - from_station = stations.get(EUROSTAR_HOME_FEEDER_ROUTE[0]) - if from_station is None: - return - - add_coordinate_if_missing(coordinates, coordinate_dict(from_station, "station")) - - def conference_free_days(trip: Trip) -> dict[str, tuple[int, int]]: """Return (days_before, days_after) exploration days for each conference. @@ -869,13 +771,7 @@ def read_geojson(data_dir: str, filename: str) -> str: def get_trip_routes(trip: Trip, data_dir: str) -> list[StrDict]: """Get routes for given trip to show on map.""" routes: list[StrDict] = [] - seen_geojson: set[str] = set() - eurostar_home_feeder_route = get_eurostar_home_feeder_route( - trip, data_dir, seen_geojson - ) - if eurostar_home_feeder_route is not None: - routes.append(eurostar_home_feeder_route) - + seen_geojson = set() for t in trip.travel: if t["type"] == "ferry": ferry_from, ferry_to = t["from_terminal"], t["to_terminal"] @@ -948,9 +844,34 @@ def get_trip_routes(trip: Trip, data_dir: str) -> list[StrDict]: if t["type"] == "train": for leg in t["legs"]: train_from, train_to = leg["from_station"], leg["to_station"] - route = build_train_route(train_from, train_to, seen_geojson) - if route is not None: - routes.append(route) + geojson_filename = train_from.get("routes", {}).get(train_to["name"]) + key = "_".join( + ["train"] + sorted([train_from["name"], train_to["name"]]) + ) + if not geojson_filename: + routes.append( + { + "type": "train", + "key": key, + "from": latlon_tuple(train_from), + "to": latlon_tuple(train_to), + } + ) + continue + + if geojson_filename in seen_geojson: + continue + seen_geojson.add(geojson_filename) + + routes.append( + { + "type": "train", + "key": key, + "geojson_filename": os.path.join( + "train_routes", geojson_filename + ), + } + ) if routes: return routes @@ -1003,15 +924,12 @@ def get_coordinates_and_routes( seen_routes: set[str] = set() for trip in trip_list: for stop in collect_trip_coordinates(trip): - key = coordinate_key(stop) + key = (stop["type"], stop["name"]) if key in seen_coordinates: continue coordinates.append(stop) seen_coordinates.add(key) - add_coordinates_for_eurostar_home_feeder(trip, coordinates, data_dir) - seen_coordinates = {coordinate_key(stop) for stop in coordinates} - for route in get_trip_routes(trip, data_dir): if route["key"] in seen_routes: continue diff --git a/static/js/map.js b/static/js/map.js index 3469fed..b47aa20 100644 --- a/static/js/map.js +++ b/static/js/map.js @@ -59,46 +59,12 @@ function emojiIcon(emoji, zoom) { function build_map(map_id, coordinates, routes) { var bounds = coordinates.map(function(station) { return [station.latitude, station.longitude]; }); - - function addGeoJsonCoordinatesToBounds(geojsonCoordinates) { - if (!geojsonCoordinates) return; - if (typeof geojsonCoordinates[0] === "number" && typeof geojsonCoordinates[1] === "number") { - bounds.push([geojsonCoordinates[1], geojsonCoordinates[0]]); - return; - } - geojsonCoordinates.forEach(addGeoJsonCoordinatesToBounds); + if (bounds.length === 0) { + routes.forEach(function(r) { + if (r.from) bounds.push(r.from); + if (r.to) bounds.push(r.to); + }); } - - function addGeoJsonGeometryToBounds(geometry) { - if (!geometry) return; - if (geometry.type === "GeometryCollection") { - geometry.geometries.forEach(addGeoJsonGeometryToBounds); - return; - } - addGeoJsonCoordinatesToBounds(geometry.coordinates); - } - - function addGeoJsonToBounds(geojson) { - if (!geojson) return; - if (geojson.type === "FeatureCollection") { - geojson.features.forEach(function(feature) { - addGeoJsonGeometryToBounds(feature.geometry); - }); - return; - } - if (geojson.type === "Feature") { - addGeoJsonGeometryToBounds(geojson.geometry); - return; - } - addGeoJsonGeometryToBounds(geojson); - } - - routes.forEach(function(r) { - if (r.from) bounds.push(r.from); - if (r.to) bounds.push(r.to); - if (r.geojson) addGeoJsonToBounds(JSON.parse(r.geojson)); - }); - var map = bounds.length > 0 ? L.map(map_id).fitBounds(bounds) : L.map(map_id).setView([20, 0], 2); diff --git a/tests/test_trip.py b/tests/test_trip.py index c3657c1..3301072 100644 --- a/tests/test_trip.py +++ b/tests/test_trip.py @@ -6,7 +6,7 @@ from datetime import date, datetime, timezone import agenda.trip import pytest -from agenda.types import StrDict, Trip +from agenda.types import Trip from web_view import app @@ -171,150 +171,6 @@ def test_get_trip_routes_assumes_unbooked_paris_trip_is_by_train( ] -def test_get_trip_routes_adds_bristol_feeder_for_london_eurostar( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Booked London Eurostar trips should show the assumed Bristol rail feeder.""" - london_st_pancras = { - "name": "London St Pancras", - "latitude": 51.531921, - "longitude": -0.126361, - "routes": {"Paris Gare du Nord": "London_St_Pancras_to_Paris_Gare_du_Nord"}, - } - paris_gare_du_nord = { - "name": "Paris Gare du Nord", - "latitude": 48.88111111111111, - "longitude": 2.355277777777778, - "routes": {"London St Pancras": "London_St_Pancras_to_Paris_Gare_du_Nord"}, - } - bristol_temple_meads = { - "name": "Bristol Temple Meads", - "latitude": 51.449, - "longitude": -2.581, - "routes": {"London Paddington": "gwml"}, - } - london_paddington = { - "name": "London Paddington", - "latitude": 51.516, - "longitude": -0.176, - "routes": {"Bristol Temple Meads": "gwml"}, - } - trip = Trip( - start=date(2026, 7, 20), - travel=[ - { - "type": "train", - "from": "London St Pancras", - "to": "Paris Gare du Nord", - "operator": "Eurostar", - "from_station": london_st_pancras, - "to_station": paris_gare_du_nord, - "legs": [ - { - "from": "London St Pancras", - "to": "Paris Gare du Nord", - "operator": "Eurostar", - "from_station": london_st_pancras, - "to_station": paris_gare_du_nord, - } - ], - } - ], - ) - stations = [ - london_st_pancras, - paris_gare_du_nord, - bristol_temple_meads, - london_paddington, - ] - - def fake_parse_yaml(name: str, data_dir: str) -> object: - if name == "stations": - return stations - raise AssertionError(f"unexpected YAML load: {name}") - - monkeypatch.setattr(agenda.trip.travel, "parse_yaml", fake_parse_yaml) - - routes = agenda.trip.get_trip_routes(trip, "/tmp/personal-data") - - assert routes == [ - { - "type": "train", - "key": "train_Bristol Temple Meads_London Paddington", - "geojson_filename": "train_routes/gwml", - }, - { - "type": "train", - "key": "train_London St Pancras_Paris Gare du Nord", - "geojson_filename": "train_routes/London_St_Pancras_to_Paris_Gare_du_Nord", - }, - ] - - -def test_add_coordinates_for_eurostar_home_feeder_adds_bristol_station( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """London Eurostar trips should get an assumed Bristol station pin.""" - london_st_pancras = { - "name": "London St Pancras", - "latitude": 51.531921, - "longitude": -0.126361, - } - paris_gare_du_nord = { - "name": "Paris Gare du Nord", - "latitude": 48.88111111111111, - "longitude": 2.355277777777778, - } - bristol_temple_meads = { - "name": "Bristol Temple Meads", - "latitude": 51.449, - "longitude": -2.581, - } - trip = Trip( - start=date(2026, 7, 20), - travel=[ - { - "type": "train", - "from": "London St Pancras", - "to": "Paris Gare du Nord", - "operator": "Eurostar", - "from_station": london_st_pancras, - "to_station": paris_gare_du_nord, - "legs": [ - { - "from": "London St Pancras", - "to": "Paris Gare du Nord", - "operator": "Eurostar", - "from_station": london_st_pancras, - "to_station": paris_gare_du_nord, - } - ], - } - ], - ) - - def fake_parse_yaml(name: str, data_dir: str) -> object: - if name == "stations": - return [london_st_pancras, paris_gare_du_nord, bristol_temple_meads] - raise AssertionError(f"unexpected YAML load: {name}") - - monkeypatch.setattr(agenda.trip.travel, "parse_yaml", fake_parse_yaml) - - coordinates: list[StrDict] = [] - agenda.trip.add_coordinates_for_eurostar_home_feeder( - trip, coordinates, "/tmp/personal-data" - ) - - assert coordinates == [ - { - "name": "Bristol Temple Meads", - "type": "station", - "latitude": 51.449, - "longitude": -2.581, - } - ] - - def test_load_cars_infers_route_labels_and_home_marker( tmp_path: pathlib.Path, ) -> None: diff --git a/validate_yaml.py b/validate_yaml.py index 0ee272d..a24a5d5 100755 --- a/validate_yaml.py +++ b/validate_yaml.py @@ -25,7 +25,6 @@ data_dir = config.PERSONAL_DATA currencies = set(config.CURRENCIES + ["GBP"]) LatLon = Tuple[float, float] -MAX_TRIP_NIGHTS = 60 def check_currency(item: agenda.types.StrDict) -> None: @@ -103,22 +102,6 @@ def ranges_overlap( return start_a < end_b and start_b < end_a -def check_trip_duration(trip: agenda.types.Trip) -> None: - """Check trip duration is within the expected maximum.""" - if trip.end is None: - return - - nights = (trip.end - trip.start).days - if nights <= MAX_TRIP_NIGHTS: - return - - url = f"https://edwardbetts.com/agenda/trip/{trip.start.isoformat()}" - print(f"Trip is too long: {url}") - print(f" {trip.title}") - print(f" {trip.start.isoformat()} to {trip.end.isoformat()}: {nights} nights") - assert False, f"Trip is {nights} nights; maximum is {MAX_TRIP_NIGHTS}" - - def check_trips() -> None: """Check trips and ensure they are in chronological order.""" filepath = os.path.join(data_dir, "trips.yaml") @@ -147,8 +130,6 @@ def check_trips() -> None: print(len(trip_list), "trips") for trip in trip_list: - check_trip_duration(trip) - if not trip.accommodation or not trip.conferences: continue accommodation_entries: list[ diff --git a/web_view.py b/web_view.py index c64382d..2ddf9c4 100755 --- a/web_view.py +++ b/web_view.py @@ -1365,9 +1365,6 @@ def trip_page(start: str) -> str: agenda.trip.add_coordinates_for_unbooked_flights( routes, coordinates, app.config["PERSONAL_DATA"] ) - agenda.trip.add_coordinates_for_eurostar_home_feeder( - trip, coordinates, app.config["PERSONAL_DATA"] - ) for route in routes: if "geojson_filename" in route: