parent
51139a1a70
commit
88d10af3f5
|
@ -90,6 +90,38 @@ def load_ferries(
|
||||||
return 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:
|
def depart_datetime(item: StrDict) -> datetime:
|
||||||
"""Return a datetime for this travel item.
|
"""Return a datetime for this travel item.
|
||||||
|
|
||||||
|
@ -156,7 +188,8 @@ def collect_travel_items(
|
||||||
return sorted(
|
return sorted(
|
||||||
load_flights(load_flight_bookings(data_dir))
|
load_flights(load_flight_bookings(data_dir))
|
||||||
+ load_trains(data_dir, route_distances=route_distances)
|
+ 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,
|
key=depart_datetime,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -266,6 +299,8 @@ def get_locations(trip: Trip) -> dict[str, StrDict]:
|
||||||
match t["type"]:
|
match t["type"]:
|
||||||
case "train":
|
case "train":
|
||||||
station_list += stations_from_travel(t)
|
station_list += stations_from_travel(t)
|
||||||
|
case "coach":
|
||||||
|
station_list += [t["from_station"], t["to_station"]]
|
||||||
case "flight":
|
case "flight":
|
||||||
for field in "from_airport", "to_airport":
|
for field in "from_airport", "to_airport":
|
||||||
if field in t:
|
if field in t:
|
||||||
|
@ -368,33 +403,50 @@ def get_trip_routes(trip: Trip, data_dir: str) -> list[StrDict]:
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
assert t["type"] == "train"
|
if t["type"] == "coach":
|
||||||
for leg in t["legs"]:
|
coach_from, coach_to = t["from_station"], t["to_station"]
|
||||||
train_from, train_to = leg["from_station"], leg["to_station"]
|
key = "_".join(["coach"] + sorted([coach_from["name"], coach_to["name"]]))
|
||||||
geojson_filename = train_from.get("routes", {}).get(train_to["name"])
|
# Use GeoJSON route when available, otherwise draw straight line
|
||||||
key = "_".join(["train"] + sorted([train_from["name"], train_to["name"]]))
|
if t.get("geojson_filename"):
|
||||||
if not 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(
|
routes.append(
|
||||||
{
|
{
|
||||||
"type": "train",
|
"type": "train",
|
||||||
"key": key,
|
"key": key,
|
||||||
"from": latlon_tuple(train_from),
|
"geojson_filename": os.path.join("train_routes", geojson_filename),
|
||||||
"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:
|
if routes:
|
||||||
return routes
|
return routes
|
||||||
|
|
|
@ -42,6 +42,7 @@ class TripElement:
|
||||||
"train": ":train:",
|
"train": ":train:",
|
||||||
"flight": ":airplane:",
|
"flight": ":airplane:",
|
||||||
"ferry": ":ferry:",
|
"ferry": ":ferry:",
|
||||||
|
"coach": ":bus:",
|
||||||
}
|
}
|
||||||
|
|
||||||
alias = emoji_map.get(self.element_type)
|
alias = emoji_map.get(self.element_type)
|
||||||
|
@ -333,6 +334,24 @@ class Trip:
|
||||||
end_country=to_country,
|
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))
|
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
|
// Draw routes
|
||||||
routes.forEach(function(route) {
|
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 };
|
var style = { weight: 3, opacity: 0.5, color: color };
|
||||||
if (route.geojson) {
|
if (route.geojson) {
|
||||||
L.geoJSON(JSON.parse(route.geojson), {
|
L.geoJSON(JSON.parse(route.geojson), {
|
||||||
|
|
|
@ -238,6 +238,47 @@ https://www.flightradar24.com/data/flights/{{ flight.airline_detail.iata | lower
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% 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) %}
|
{% macro ferry_row(item) %}
|
||||||
<div class="grid-item text-end">{{ item.depart.strftime("%a, %d %b %Y") }}</div>
|
<div class="grid-item text-end">{{ item.depart.strftime("%a, %d %b %Y") }}</div>
|
||||||
<div class="grid-item">
|
<div class="grid-item">
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
{% block title %}{{ trip.title }} ({{ display_date(trip.start) }}) - Edward Betts{% endblock %}
|
{% 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() %}
|
{% macro next_and_previous() %}
|
||||||
<p>
|
<p>
|
||||||
|
@ -271,6 +271,11 @@
|
||||||
{{ item.from }}
|
{{ item.from }}
|
||||||
→
|
→
|
||||||
{{ item.to }}
|
{{ item.to }}
|
||||||
|
{% elif item.type == "coach" %}
|
||||||
|
🚌
|
||||||
|
{{ item.from }}
|
||||||
|
→
|
||||||
|
{{ item.to }}
|
||||||
{% elif item.type == "ferry" %}
|
{% elif item.type == "ferry" %}
|
||||||
⛴️
|
⛴️
|
||||||
{{ item.from }}
|
{{ item.from }}
|
||||||
|
@ -317,6 +322,22 @@
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</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" %}
|
{% elif item.type == "ferry" %}
|
||||||
<div>
|
<div>
|
||||||
<span>{{ item.operator }} - {{ item.ferry }}</span>
|
<span>{{ item.operator }} - {{ item.ferry }}</span>
|
||||||
|
|
Loading…
Reference in a new issue