Add destination time difference and live local times on trip pages
Closes #208
This commit is contained in:
parent
016039e78f
commit
ec413ac310
2 changed files with 168 additions and 0 deletions
|
|
@ -99,6 +99,31 @@
|
|||
<div class="mb-3">
|
||||
{# <div>Countries: {{ trip.countries_str }}</div> #}
|
||||
<div>Locations: {{ trip.locations_str }}</div>
|
||||
{% if destination_times %}
|
||||
<div class="mt-2">
|
||||
<strong>Destination time zones</strong>
|
||||
<table class="table table-sm table-hover w-auto mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Destination</th>
|
||||
<th>Timezone</th>
|
||||
<th>Difference from UK</th>
|
||||
<th>Current local time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in destination_times %}
|
||||
<tr>
|
||||
<td>{{ item.location }} ({{ item.country_name }}) {{ item.country_flag if trip.show_flags }}</td>
|
||||
<td>{{ item.timezone or "Unknown" }}</td>
|
||||
<td class="destination-offset" {% if item.timezone %}data-timezone="{{ item.timezone }}"{% endif %}>{{ item.offset_display }}</td>
|
||||
<td class="destination-time" {% if item.timezone %}data-timezone="{{ item.timezone }}"{% endif %}>{{ item.current_time or "Unknown" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if total_distance %}
|
||||
<div>Total distance:
|
||||
|
|
@ -465,5 +490,46 @@ var routes = {{ routes | tojson }};
|
|||
|
||||
build_map("map", coordinates, routes);
|
||||
|
||||
function timezoneOffsetLabel(offsetMinutes) {
|
||||
if (offsetMinutes === 0) {
|
||||
return "Same time as Bristol";
|
||||
}
|
||||
const sign = offsetMinutes > 0 ? "+" : "-";
|
||||
const abs = Math.abs(offsetMinutes);
|
||||
const hours = String(Math.floor(abs / 60)).padStart(2, "0");
|
||||
const minutes = String(abs % 60).padStart(2, "0");
|
||||
return `${sign}${hours}:${minutes} vs Bristol`;
|
||||
}
|
||||
|
||||
function getOffsetMinutes(timeZone) {
|
||||
const now = new Date();
|
||||
const localInZone = new Date(now.toLocaleString("en-US", { timeZone }));
|
||||
const localInBristol = new Date(now.toLocaleString("en-US", { timeZone: "Europe/London" }));
|
||||
return Math.round((localInZone - localInBristol) / 60000);
|
||||
}
|
||||
|
||||
function updateDestinationTimes() {
|
||||
for (const el of document.querySelectorAll(".destination-time[data-timezone]")) {
|
||||
const tz = el.dataset.timezone;
|
||||
el.textContent = new Intl.DateTimeFormat("en-GB", {
|
||||
timeZone: tz,
|
||||
weekday: "short",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: false
|
||||
}).format(new Date());
|
||||
}
|
||||
|
||||
for (const el of document.querySelectorAll(".destination-offset[data-timezone]")) {
|
||||
const tz = el.dataset.timezone;
|
||||
const offset = getOffsetMinutes(tz);
|
||||
el.textContent = timezoneOffsetLabel(offset);
|
||||
}
|
||||
}
|
||||
|
||||
updateDestinationTimes();
|
||||
setInterval(updateDestinationTimes, 1000);
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
102
web_view.py
102
web_view.py
|
|
@ -11,10 +11,13 @@ import os.path
|
|||
import sys
|
||||
import time
|
||||
import traceback
|
||||
import typing
|
||||
from collections import defaultdict
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import flask
|
||||
import pytz
|
||||
import werkzeug
|
||||
import werkzeug.debug.tbtools
|
||||
import yaml
|
||||
|
|
@ -761,6 +764,104 @@ def get_prev_current_and_next_trip(
|
|||
return (prev_trip, current_trip, next_trip)
|
||||
|
||||
|
||||
def _timezone_name_from_datetime(value: typing.Any) -> str | None:
|
||||
"""Get IANA timezone name from a datetime value if available."""
|
||||
if not isinstance(value, datetime) or value.tzinfo is None:
|
||||
return None
|
||||
|
||||
key = getattr(value.tzinfo, "key", None)
|
||||
if isinstance(key, str):
|
||||
return key
|
||||
|
||||
zone = getattr(value.tzinfo, "zone", None)
|
||||
if isinstance(zone, str):
|
||||
return zone
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _format_offset_from_bristol(offset_minutes: int) -> str:
|
||||
"""Format offset from Bristol in +/-HH:MM."""
|
||||
if offset_minutes == 0:
|
||||
return "Same time as Bristol"
|
||||
sign = "+" if offset_minutes > 0 else "-"
|
||||
hours, mins = divmod(abs(offset_minutes), 60)
|
||||
return f"{sign}{hours:02d}:{mins:02d} vs Bristol"
|
||||
|
||||
|
||||
def get_destination_timezones(trip: Trip) -> list[StrDict]:
|
||||
"""Build destination timezone metadata for the trip page."""
|
||||
per_location: dict[tuple[str, str], list[str]] = defaultdict(list)
|
||||
for item in trip.accommodation + trip.conferences + trip.events:
|
||||
location = item.get("location")
|
||||
country = item.get("country")
|
||||
if not isinstance(location, str) or not isinstance(country, str):
|
||||
continue
|
||||
|
||||
key = (location, country.lower())
|
||||
timezone_name = item.get("timezone")
|
||||
if isinstance(timezone_name, str):
|
||||
per_location[key].append(timezone_name)
|
||||
|
||||
for field in (
|
||||
"from",
|
||||
"to",
|
||||
"date",
|
||||
"start",
|
||||
"end",
|
||||
"attend_start",
|
||||
"attend_end",
|
||||
):
|
||||
candidate = _timezone_name_from_datetime(item.get(field))
|
||||
if candidate:
|
||||
per_location[key].append(candidate)
|
||||
|
||||
home_now = datetime.now(ZoneInfo("Europe/London"))
|
||||
destination_times: list[StrDict] = []
|
||||
|
||||
for location, country in trip.locations():
|
||||
country_code = country.alpha_2.lower()
|
||||
key = (location, country_code)
|
||||
timezone_name = None
|
||||
|
||||
for candidate in per_location.get(key, []):
|
||||
try:
|
||||
ZoneInfo(candidate)
|
||||
timezone_name = candidate
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not timezone_name:
|
||||
country_timezones = pytz.country_timezones.get(country_code, [])
|
||||
if len(country_timezones) == 1:
|
||||
timezone_name = country_timezones[0]
|
||||
|
||||
offset_display = "Timezone unknown"
|
||||
current_time = None
|
||||
if timezone_name:
|
||||
dest_now = datetime.now(ZoneInfo(timezone_name))
|
||||
dest_offset = dest_now.utcoffset()
|
||||
home_offset = home_now.utcoffset()
|
||||
if dest_offset is not None and home_offset is not None:
|
||||
offset_minutes = int((dest_offset - home_offset).total_seconds() // 60)
|
||||
offset_display = _format_offset_from_bristol(offset_minutes)
|
||||
current_time = dest_now.strftime("%a %H:%M:%S")
|
||||
|
||||
destination_times.append(
|
||||
{
|
||||
"location": location,
|
||||
"country_name": country.name,
|
||||
"country_flag": country.flag,
|
||||
"timezone": timezone_name,
|
||||
"offset_display": offset_display,
|
||||
"current_time": current_time,
|
||||
}
|
||||
)
|
||||
|
||||
return destination_times
|
||||
|
||||
|
||||
@app.route("/trip/<start>")
|
||||
def trip_page(start: str) -> str:
|
||||
"""Individual trip page."""
|
||||
|
|
@ -802,6 +903,7 @@ def trip_page(start: str) -> str:
|
|||
format_list_with_ampersand=format_list_with_ampersand,
|
||||
holidays=agenda.holidays.get_trip_holidays(trip),
|
||||
school_holidays=agenda.holidays.get_trip_school_holidays(trip),
|
||||
destination_times=get_destination_timezones(trip),
|
||||
human_readable_delta=agenda.utils.human_readable_delta,
|
||||
trip_weather=trip_weather,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue