parent
51139a1a70
commit
88d10af3f5
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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), {
|
||||
|
|
|
@ -238,6 +238,47 @@ https://www.flightradar24.com/data/flights/{{ flight.airline_detail.iata | lower
|
|||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro coach_row(item) %}
|
||||
{% set url = item.url %}
|
||||
<div class="grid-item text-end">{{ item.depart.strftime("%a, %d %b %Y") }}</div>
|
||||
<div class="grid-item">
|
||||
{% if g.user.is_authenticated and item.url %}<a href="{{ url }}">{% endif %}
|
||||
{{ item.from }} → {{ item.to }}
|
||||
{% if g.user.is_authenticated and item.url %}</a>{% endif %}
|
||||
</div>
|
||||
<div class="grid-item">{{ item.depart.strftime("%H:%M") }}</div>
|
||||
<div class="grid-item">
|
||||
{% if item.arrive %}
|
||||
{{ item.arrive.strftime("%H:%M") }}
|
||||
{% if item.depart != item.arrive and item.arrive.date() != item.depart.date() %}+1 day{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="grid-item">{{ ((item.arrive - item.depart).total_seconds() // 60) | int }} mins</div>
|
||||
<div class="grid-item">{{ item.operator }}</div>
|
||||
<div class="grid-item">
|
||||
{% if g.user.is_authenticated %}
|
||||
{{ item.booking_reference }}
|
||||
{% else %}
|
||||
<em>redacted</em>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
</div>
|
||||
<div class="grid-item text-end">
|
||||
{% if item.distance %}
|
||||
{{ "{:,.0f} km / {:,.0f} miles".format(item.distance, item.distance / 1.60934) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="grid-item text-end">
|
||||
{% if g.user.is_authenticated and item.price and item.currency %}
|
||||
<span class="badge bg-info text-nowrap">{{ "{:,f}".format(item.price) }} {{ item.currency }}</span>
|
||||
{% if item.currency != "GBP" and item.currency in fx_rate %}
|
||||
<span class="badge bg-info text-nowrap">{{ "{:,.2f}".format(item.price / fx_rate[item.currency]) }} GBP</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro ferry_row(item) %}
|
||||
<div class="grid-item text-end">{{ item.depart.strftime("%a, %d %b %Y") }}</div>
|
||||
<div class="grid-item">
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
{% block title %}{{ trip.title }} ({{ display_date(trip.start) }}) - Edward Betts{% endblock %}
|
||||
|
||||
{% from "macros.html" import trip_link, display_datetime, display_date_no_year, display_date, conference_row, accommodation_row, flight_row, train_row with context %}
|
||||
{% from "macros.html" import trip_link, display_datetime, display_date_no_year, display_date, conference_row, accommodation_row, flight_row, train_row, ferry_row, coach_row with context %}
|
||||
|
||||
{% set row = { "flight": flight_row, "train": train_row } %}
|
||||
{% set row = {"flight": flight_row, "train": train_row, "ferry": ferry_row, "coach": coach_row} %}
|
||||
|
||||
{% macro next_and_previous() %}
|
||||
<p>
|
||||
|
@ -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 @@
|
|||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif item.type == "coach" %}
|
||||
<div>
|
||||
{{ display_datetime(item.depart) }}
|
||||
→
|
||||
{{ item.arrive.strftime("%H:%M %z") }}
|
||||
{% if item.class %}
|
||||
<span class="badge bg-info text-nowrap">{{ item.class }}</span>
|
||||
{% endif %}
|
||||
<span>🕒{{ ((item.arrive - item.depart).total_seconds() // 60) | int }} mins</span>
|
||||
{% if item.distance %}
|
||||
<span>
|
||||
🛤️
|
||||
{{ "{:,.0f} km / {:,.0f} miles".format(item.distance, item.distance / 1.60934) }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif item.type == "ferry" %}
|
||||
<div>
|
||||
<span>{{ item.operator }} - {{ item.ferry }}</span>
|
||||
|
|
Loading…
Reference in a new issue