diff --git a/agenda/trip.py b/agenda/trip.py index b1cf69f..978434d 100644 --- a/agenda/trip.py +++ b/agenda/trip.py @@ -90,6 +90,38 @@ def load_ferries( return ferries +def load_coaches( + data_dir: str, route_distances: travel.RouteDistances | None = None +) -> list[StrDict]: + """Load coaches.""" + coaches = load_travel("coach", "coaches", data_dir) + stations = travel.parse_yaml("coach_stations", data_dir) + by_name = {station["name"]: station for station in stations} + + for item in coaches: + add_station_objects(item, by_name) + # Add route distance and CO2 calculation if available + if route_distances: + travel.add_leg_route_distance(item, route_distances) + if "distance" in item: + # Calculate CO2 emissions for coach (0.027 kg CO2e per passenger per km) + item["co2_kg"] = item["distance"] * 0.027 + # Include GeoJSON route if defined in stations data + from_station = item.get("from_station") + to_station = item.get("to_station") + if from_station and to_station: + # Support scalar or mapping routes: string or dict of station name -> geojson base name + routes_val = from_station.get("routes", {}) + if isinstance(routes_val, str): + geo = routes_val + else: + geo = routes_val.get(to_station.get("name")) + if geo: + item["geojson_filename"] = geo + + return coaches + + def depart_datetime(item: StrDict) -> datetime: """Return a datetime for this travel item. @@ -156,7 +188,8 @@ def collect_travel_items( return sorted( load_flights(load_flight_bookings(data_dir)) + load_trains(data_dir, route_distances=route_distances) - + load_ferries(data_dir, route_distances=route_distances), + + load_ferries(data_dir, route_distances=route_distances) + + load_coaches(data_dir, route_distances=route_distances), key=depart_datetime, ) @@ -266,6 +299,8 @@ def get_locations(trip: Trip) -> dict[str, StrDict]: match t["type"]: case "train": station_list += stations_from_travel(t) + case "coach": + station_list += [t["from_station"], t["to_station"]] case "flight": for field in "from_airport", "to_airport": if field in t: @@ -368,33 +403,50 @@ def get_trip_routes(trip: Trip, data_dir: str) -> list[StrDict]: } ) continue - assert t["type"] == "train" - for leg in t["legs"]: - train_from, train_to = leg["from_station"], leg["to_station"] - geojson_filename = train_from.get("routes", {}).get(train_to["name"]) - key = "_".join(["train"] + sorted([train_from["name"], train_to["name"]])) - if not geojson_filename: + if t["type"] == "coach": + coach_from, coach_to = t["from_station"], t["to_station"] + key = "_".join(["coach"] + sorted([coach_from["name"], coach_to["name"]])) + # Use GeoJSON route when available, otherwise draw straight line + if t.get("geojson_filename"): + filename = os.path.join("coach_routes", t["geojson_filename"]) + routes.append({"type": "coach", "key": key, "geojson_filename": filename}) + else: + routes.append( + { + "type": "coach", + "key": key, + "from": latlon_tuple(coach_from), + "to": latlon_tuple(coach_to), + } + ) + continue + if t["type"] == "train": + for leg in t["legs"]: + train_from, train_to = leg["from_station"], leg["to_station"] + 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, - "from": latlon_tuple(train_from), - "to": latlon_tuple(train_to), + "geojson_filename": os.path.join("train_routes", geojson_filename), } ) - 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 diff --git a/agenda/types.py b/agenda/types.py index 67a9545..94830fd 100644 --- a/agenda/types.py +++ b/agenda/types.py @@ -42,6 +42,7 @@ class TripElement: "train": ":train:", "flight": ":airplane:", "ferry": ":ferry:", + "coach": ":bus:", } alias = emoji_map.get(self.element_type) @@ -333,6 +334,24 @@ class Trip: end_country=to_country, ) ) + if item["type"] == "coach": + # Include coach journeys in trip elements + from_country = agenda.get_country(item["from_station"]["country"]) + to_country = agenda.get_country(item["to_station"]["country"]) + name = f"{item['from']} → {item['to']}" + elements.append( + TripElement( + start_time=item["depart"], + end_time=item.get("arrive"), + title=name, + detail=item, + element_type="coach", + start_loc=item["from"], + end_loc=item["to"], + start_country=from_country, + end_country=to_country, + ) + ) return sorted(elements, key=lambda e: utils.as_datetime(e.start_time)) diff --git a/static/js/map.js b/static/js/map.js index 8ae88a6..73ca7d1 100644 --- a/static/js/map.js +++ b/static/js/map.js @@ -140,7 +140,7 @@ function build_map(map_id, coordinates, routes) { // Draw routes routes.forEach(function(route) { - var color = {"train": "blue", "flight": "red", "unbooked_flight": "orange"}[route.type]; + var color = {"train": "blue", "flight": "red", "unbooked_flight": "orange", "coach": "green"}[route.type]; var style = { weight: 3, opacity: 0.5, color: color }; if (route.geojson) { L.geoJSON(JSON.parse(route.geojson), { diff --git a/templates/macros.html b/templates/macros.html index ff49986..5335d62 100644 --- a/templates/macros.html +++ b/templates/macros.html @@ -238,6 +238,47 @@ https://www.flightradar24.com/data/flights/{{ flight.airline_detail.iata | lower {% endmacro %} +{% macro coach_row(item) %} + {% set url = item.url %} +
@@ -271,6 +271,11 @@ {{ item.from }} → {{ item.to }} + {% elif item.type == "coach" %} + 🚌 + {{ item.from }} + → + {{ item.to }} {% elif item.type == "ferry" %} ⛴️ {{ item.from }} @@ -317,6 +322,22 @@ {% endif %}