agenda/templates/trip_page.html
Edward Betts b4f0a5bf5d Add OpenWeatherMap weather forecasts. Closes #48
Show 8-day Bristol home weather on the index and weekends pages.
Show destination weather per day on the trip list and trip page.
Cache forecasts in ~/lib/data/weather/ and refresh via update.py.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 20:27:25 +00:00

469 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% 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, display_conf_date_no_year, conference_row, accommodation_row, flight_row, train_row, ferry_row, coach_row, bus_row with context %}
{% set row = {"flight": flight_row, "train": train_row, "ferry": ferry_row, "coach": coach_row, "bus": bus_row} %}
{% macro next_and_previous() %}
<p>
{% if prev_trip %}
previous: {{ trip_link(prev_trip) }} ({{ (trip.start - prev_trip.end).days }} days)
{% endif %}
{% if next_trip %}
next: {{ trip_link(next_trip) }} ({{ (next_trip.start - trip.end).days }} days)
{% endif %}
</p>
{% endmacro %}
{% block style %}
{% if coordinates %}
<link rel="stylesheet" href="{{ url_for("static", filename="leaflet/leaflet.css") }}">
{% endif %}
{% set conference_column_count = 7 %}
{% set accommodation_column_count = 7 %}
{% set travel_column_count = 9 %}
<style>
.conferences {
display: grid;
grid-template-columns: repeat({{ conference_column_count }}, auto); /* 7 columns for each piece of information */
gap: 10px;
justify-content: start;
}
.accommodation {
display: grid;
grid-template-columns: repeat({{ accommodation_column_count }}, auto);
gap: 10px;
justify-content: start;
}
.travel {
display: grid;
grid-template-columns: repeat({{ travel_column_count }}, auto);
gap: 10px;
justify-content: start;
}
.grid-item {
/* Additional styling for grid items can go here */
}
.half-map {
height: 90vh;
}
.full-window-map {
position: fixed; /* Make the map fixed position */
top: 56px;
left: 0;
right: 0;
bottom: 0;
z-index: 9999; /* Make sure it sits on top */
}
#toggleMapSize {
position: fixed; /* Fixed position */
top: 66px; /* 10px from the top */
right: 10px; /* 10px from the right */
z-index: 10000; /* Higher than the map's z-index */
}
</style>
{% endblock %}
{% set end = trip.end %}
{% set total_distance = trip.total_distance() %}
{% set distances_by_transport_type = trip.distances_by_transport_type() %}
{% set total_co2_kg = trip.total_co2_kg() %}
{% set co2_by_transport_type = trip.co2_by_transport_type() %}
{% block content %}
<div class="row">
<div class="col-md-6 col-sm-12">
<div class="m-3">
{{ next_and_previous() }}
<h1>{{ trip.title }}</h1>
<p class="lead">
{% if end %}
{{ display_date_no_year(trip.start) }} to {{ display_date_no_year(end) }}
({{ (end - trip.start).days }} nights)
{% else %}
{{ display_date_no_year(trip.start) }} (end date missing)
{% endif %}
</p>
<div class="mb-3">
{# <div>Countries: {{ trip.countries_str }}</div> #}
<div>Locations: {{ trip.locations_str }}</div>
{% if total_distance %}
<div>Total distance:
{{ "{:,.0f} km / {:,.0f} miles".format(total_distance, total_distance / 1.60934) }}
</div>
{% endif %}
{% if distances_by_transport_type %}
{% for transport_type, distance in distances_by_transport_type %}
<div>{{ transport_type | title }} distance:
{{ "{:,.0f} km / {:,.0f} miles".format(distance, distance / 1.60934) }}
</div>
{% endfor %}
{% endif %}
{% if total_co2_kg %}
<div>Total CO₂: {{ "{:,.1f}".format(total_co2_kg) }} kg</div>
{% endif %}
{% if co2_by_transport_type %}
{% for transport_type, co2_kg in co2_by_transport_type %}
<div>{{ transport_type | title }} CO₂:
{{ "{:,.1f}".format(co2_kg) }} kg
</div>
{% endfor %}
{% endif %}
{% set delta = human_readable_delta(trip.start) %}
{% if delta %}
<div>How long until trip: {{ delta }}</div>
{% endif %}
{% if trip.schengen_compliance %}
<div class="mt-3">
<strong>Schengen Compliance:</strong>
{% if trip.schengen_compliance.is_compliant %}
<span class="badge bg-success">✅ Compliant</span>
{% else %}
<span class="badge bg-danger">❌ Non-compliant</span>
{% endif %}
<div class="text-muted small">
{{ trip.schengen_compliance.total_days_used }}/90 days used
{% if trip.schengen_compliance.is_compliant %}
({{ trip.schengen_compliance.days_remaining }} remaining)
{% else %}
({{ trip.schengen_compliance.days_over_limit }} over limit)
{% endif %}
</div>
</div>
{% endif %}
</div>
{% for item in trip.conferences %}
{% set country = get_country(item.country) if item.country else None %}
<div class="card my-1">
<div class="card-body">
<h5 class="card-title">
<a href="{{ item.url }}">{{ item.name }}</a>
<small class="text-muted">
{{ display_conf_date_no_year(item.attend_start if item.attend_start else item.start) }} to {{ display_conf_date_no_year(item.attend_end if item.attend_end else item.end) }}
{% if item.attend_start or item.attend_end %}
(full conference: {{ display_date_no_year(item.start) }} to {{ display_date_no_year(item.end) }})
{% endif %}
</small>
</h5>
<p class="card-text">
<strong>Topic:</strong> {{ item.topic }}
<strong>Venue:</strong> {{ item.venue }}
<strong>Location:</strong> {{ item.location }}
{% if country %}
{{ country.flag if trip.show_flags }}
{% elif item.online %}
💻 Online
{% else %}
<span class="text-bg-danger p-2">
country code <strong>{{ item.country }}</strong> not found
</span>
{% endif %}
{% if item.free %}
<span class="badge bg-success text-nowrap">free to attend</span>
{% elif item.price and item.currency %}
<span class="badge bg-info text-nowrap">price: {{ item.price }} {{ item.currency }}</span>
{% endif %}
</p>
</div>
</div>
{% endfor %}
{% for item in trip.accommodation %}
{% set country = get_country(item.country) if item.country else None %}
{% set nights = (item.to.date() - item.from.date()).days %}
<div class="card my-1">
<div class="card-body">
<h5 class="card-title">
{% if item.operator %}{{ item.operator }}: {% endif %}
<a href="{{ item.url }}">{{ item.name }}</a>
<small class="text-muted">
{{ display_date_no_year(item.from) }} to {{ display_date_no_year(item.to) }}
({% if nights == 1 %}1 night{% else %}{{ nights }} nights{% endif %})
</small>
</h5>
<p class="card-text">
<strong>Address:</strong> {{ item.address }}
<strong>Location:</strong> {{ item.location }}
{% if country %}
{{ country.flag if trip.show_flags }}
{% else %}
<span class="text-bg-danger p-2">
country code <strong>{{ item.country }}</strong> not found
</span>
{% endif %}
{% if g.user.is_authenticated and item.price and item.currency %}
<span class="badge bg-info text-nowrap">price: {{ item.price }} {{ item.currency }}</span>
{% endif %}
</p>
</div>
</div>
{% endfor %}
{% if trip.flight_bookings %}
<h3>Flight bookings</h3>
{% for item in trip.flight_bookings %}
<div>
{{ item.flights | map(attribute="airline_name") | unique | join(" + ") }}
{% if g.user.is_authenticated and item.booking_reference %}
<strong>booking reference:</strong> {{ item.booking_reference }}
{% endif %}
{% if g.user.is_authenticated and item.price and item.currency %}
<span class="badge bg-info text-nowrap">price: {{ item.price }} {{ item.currency }}</span>
{% endif %}
</div>
{% endfor %}
{% endif %}
{% for item in trip.events %}
{% set country = get_country(item.country) if item.country else None %}
<div class="card my-1">
<div class="card-body">
<h5 class="card-title">
<a href="{{ item.url }}">{{ item.title }}</a>
<small class="text-muted">{{ display_date_no_year(item.date) }}</small>
</h5>
<p class="card-text">
Address: {{ item.address }}
| Location: {{ item.location }}
{% if country %}
{{ country.flag if trip.show_flags }}
{% else %}
<span class="text-bg-danger p-2">
country code <strong>{{ item.country }}</strong> not found
</span>
{% endif %}
{% if g.user.is_authenticated and item.price and item.currency %}
| <span class="badge bg-info text-nowrap">price: {{ item.price }} {{ item.currency }}</span>
{% endif %}
</p>
</div>
</div>
{% endfor %}
{% for item in trip.travel %}
<div class="card my-1">
<div class="card-body">
<h5 class="card-title">
{% if item.type == "flight" %}
✈️
{{ item.from_airport.name }} ({{ item.from_airport.iata}})
{{ item.to_airport.name }} ({{item.to_airport.iata}})
{% elif item.type == "train" %}
🚆
{{ item.from }}
{{ item.to }}
{% elif item.type == "coach" or item.type == "bus" %}
🚌
{{ item.from }}
{{ item.to }}
{% elif item.type == "ferry" %}
⛴️
{{ item.from }}
{{ item.to }}
{% endif %}
</h5>
<p class="card-text">
{% if item.type == "flight" %}
<div>
<span>{{ item.airline_name }} ({{ item.airline }})</span>
{{ display_datetime(item.depart) }}
{% if item.arrive %}
{{ item.arrive.strftime("%H:%M %z") }}
<span>🕒{{ ((item.arrive - item.depart).total_seconds() // 60) | int }} mins</span>
{% endif %}
<span>{{ item.airline_code }}{{ item.flight_number }}</span>
{% if item.distance %}
<span>
🌍distance:
{{ "{:,.0f} km / {:,.0f} miles".format(item.distance, item.distance / 1.60934) }}
</span>
{% endif %}
</div>
{% elif item.type == "train" %}
<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 == "coach" or item.type == "bus" %}
<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>
{{ display_datetime(item.depart) }}
{{ item.arrive.strftime("%H:%M %z") }}
<span>🕒{{ ((item.arrive - item.depart).total_seconds() // 60) | int }} mins</span>
{% if item.class %}
<span class="badge bg-info text-nowrap">{{ item.class }}</span>
{% endif %}
</div>
{% if item.vehicle %}
<div>
🚗 Vehicle: {{ item.vehicle.type }} {% if g.user.is_authenticated %}({{ item.vehicle.registration }}) {% endif %}
{% if item.vehicle.extras %}
- Extras: {{ item.vehicle.extras | join(", ") }}
{% endif %}
</div>
{% endif %}
{% if g.user.is_authenticated %}
<div>
{% if item.booking_reference %}
<strong>Booking reference:</strong> {{ item.booking_reference }}
{% endif %}
{% if item.price and item.currency %}
<span class="badge bg-info text-nowrap">Price: {{ item.price }} {{ item.currency }}</span>
{% endif %}
</div>
{% endif %}
{% endif %}
</p>
</div>
</div>
{% endfor %}
{% if trip_weather %}
<div class="mt-3">
<h4>Weather forecast</h4>
{% set ns = namespace(has_rows=false) %}
<table class="table table-hover w-auto">
{% for day, elements in trip.elements_grouped_by_day() %}
{% set weather = trip_weather.get(day.isoformat()) %}
{% if weather %}
{% set ns.has_rows = true %}
<tr>
<td class="text-end text-nowrap">{{ display_date(day) }}</td>
<td><img src="https://openweathermap.org/img/wn/{{ weather.icon }}.png" alt="{{ weather.status }}" title="{{ weather.detailed_status }}" width="25" height="25"></td>
<td>{{ weather.temp_min }}{{ weather.temp_max }}°C</td>
<td>{{ weather.detailed_status }}</td>
</tr>
{% endif %}
{% endfor %}
</table>
{% if not ns.has_rows %}
<p class="text-muted">Forecast not yet available (available up to 8 days ahead).</p>
{% endif %}
</div>
{% endif %}
<div class="mt-3">
<h4>Holidays</h4>
{% if holidays %}
<table class="table table-hover w-auto">
{% for item in holidays %}
{% set country = get_country(item.country) %}
<tr>
{% if loop.first or item.date != loop.previtem.date %}
<td class="text-end">{{ display_date(item.date) }}</td>
{% else %}
<td></td>
{% endif %}
<td>{{ country.flag if trip.show_flags }} {{ country.name }}</td>
<td>{{ item.display_name }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<p>No public holidays during trip.</p>
{% endif %}
</div>
<div class="mt-3">
<h4>UK school holidays (Bristol)</h4>
{% if school_holidays %}
<table class="table table-hover w-auto">
{% for item in school_holidays %}
<tr>
<td class="text-end">{{ display_date(item.as_date) }}</td>
<td>to {{ display_date(item.end_as_date) }}</td>
<td>{{ item.title }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<p>No UK school holidays during trip.</p>
{% endif %}
</div>
{{ next_and_previous() }}
</div>
</div>
<div class="col-md-6 col-sm-12">
<button id="toggleMapSize" class="btn btn-primary mb-2">Toggle map size</button>
<div id="map" class="half-map">
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for("static", filename="leaflet/leaflet.js") }}"></script>
<script src="{{ url_for("static", filename="leaflet-geodesic/leaflet.geodesic.umd.min.js") }}"></script>
<script src="{{ url_for("static", filename="js/map.js") }}"></script>
<script>
var coordinates = {{ coordinates | tojson }};
var routes = {{ routes | tojson }};
build_map("map", coordinates, routes);
</script>
{% endblock %}